Skip to main content
โšก Calmops

Interface Composition and Embedding

Interface Composition and Embedding

Go’s composition model allows you to combine interfaces and embed types to create flexible, reusable designs. This guide covers interface composition and embedding patterns.

Interface Composition

Combining Interfaces

package main

import "fmt"

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Compose interfaces
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Implementation
type File struct {
    name string
}

func (f *File) Read(p []byte) (n int, err error) {
    fmt.Printf("Reading from %s\n", f.name)
    return len(p), nil
}

func (f *File) Write(p []byte) (n int, err error) {
    fmt.Printf("Writing to %s\n", f.name)
    return len(p), nil
}

func (f *File) Close() error {
    fmt.Printf("Closing %s\n", f.name)
    return nil
}

func main() {
    f := &File{name: "data.txt"}
    
    // Can be used as ReadWriter
    var rw ReadWriter = f
    rw.Read([]byte{})
    rw.Write([]byte{})
    
    // Can be used as ReadWriteCloser
    var rwc ReadWriteCloser = f
    rwc.Read([]byte{})
    rwc.Write([]byte{})
    rwc.Close()
}

Interface Hierarchy

package main

import "fmt"

// Base interface
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Extended interface
type BufferedReader interface {
    Reader
    ReadLine() (string, error)
}

// Further extended
type AdvancedReader interface {
    BufferedReader
    ReadUntil(delimiter byte) ([]byte, error)
}

type MyReader struct{}

func (mr *MyReader) Read(p []byte) (n int, err error) {
    return 0, nil
}

func (mr *MyReader) ReadLine() (string, error) {
    return "line", nil
}

func (mr *MyReader) ReadUntil(delimiter byte) ([]byte, error) {
    return []byte{}, nil
}

func main() {
    mr := &MyReader{}
    
    var ar AdvancedReader = mr
    fmt.Println(ar.ReadLine())
}

Type Embedding

Embedding Structs

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) Greet() string {
    return fmt.Sprintf("Hello, I'm %s", p.Name)
}

// Embed Person in Employee
type Employee struct {
    Person  // Embedded field
    Company string
    Salary  float64
}

func main() {
    emp := Employee{
        Person: Person{Name: "Alice", Age: 30},
        Company: "TechCorp",
        Salary: 100000,
    }
    
    // Access embedded fields directly
    fmt.Println(emp.Name)      // Alice
    fmt.Println(emp.Age)       // 30
    fmt.Println(emp.Company)   // TechCorp
    
    // Call embedded methods
    fmt.Println(emp.Greet())   // Hello, I'm Alice
}

Method Promotion

package main

import "fmt"

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Embed interfaces
type ReadWriter interface {
    Reader
    Writer
}

type File struct {
    name string
}

func (f *File) Read(p []byte) (n int, err error) {
    fmt.Printf("Reading from %s\n", f.name)
    return len(p), nil
}

func (f *File) Write(p []byte) (n int, err error) {
    fmt.Printf("Writing to %s\n", f.name)
    return len(p), nil
}

// File automatically satisfies ReadWriter
func ProcessFile(rw ReadWriter) {
    rw.Read([]byte{})
    rw.Write([]byte{})
}

func main() {
    f := &File{name: "data.txt"}
    ProcessFile(f)
}

Practical Examples

Logger with Composition

package main

import (
    "fmt"
    "time"
)

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type WriteCloser interface {
    Writer
    Closer
}

type Logger struct {
    output WriteCloser
}

func (l *Logger) Info(msg string) {
    timestamp := time.Now().Format(time.RFC3339)
    l.output.Write([]byte(fmt.Sprintf("[INFO] %s: %s\n", timestamp, msg)))
}

func (l *Logger) Error(msg string) {
    timestamp := time.Now().Format(time.RFC3339)
    l.output.Write([]byte(fmt.Sprintf("[ERROR] %s: %s\n", timestamp, msg)))
}

type ConsoleWriter struct{}

func (cw *ConsoleWriter) Write(p []byte) (n int, err error) {
    fmt.Print(string(p))
    return len(p), nil
}

func (cw *ConsoleWriter) Close() error {
    return nil
}

func main() {
    logger := &Logger{output: &ConsoleWriter{}}
    logger.Info("Application started")
    logger.Error("Something went wrong")
}

Database with Composition

package main

import "fmt"

type Executor interface {
    Execute(query string) error
}

type Querier interface {
    Query(query string) ([]map[string]interface{}, error)
}

type Database interface {
    Executor
    Querier
}

type PostgreSQL struct {
    connection string
}

func (pg *PostgreSQL) Execute(query string) error {
    fmt.Printf("Executing on PostgreSQL: %s\n", query)
    return nil
}

func (pg *PostgreSQL) Query(query string) ([]map[string]interface{}, error) {
    fmt.Printf("Querying PostgreSQL: %s\n", query)
    return []map[string]interface{}{}, nil
}

type Repository struct {
    db Database
}

func (r *Repository) CreateUser(name string) error {
    return r.db.Execute(fmt.Sprintf("INSERT INTO users (name) VALUES ('%s')", name))
}

func (r *Repository) GetUsers() ([]map[string]interface{}, error) {
    return r.db.Query("SELECT * FROM users")
}

func main() {
    db := &PostgreSQL{connection: "localhost"}
    repo := &Repository{db: db}
    
    repo.CreateUser("Alice")
    users, _ := repo.GetUsers()
    fmt.Println(users)
}

Embedding Best Practices

โœ… Good Practices

  1. Use embedding for composition - Avoid deep inheritance
  2. Embed interfaces for flexibility - Combine small interfaces
  3. Promote methods intentionally - Clear what’s inherited
  4. Document embedded types - Explain the relationship
  5. Use embedding for code reuse - Share common behavior
  6. Keep embedding shallow - Avoid complex hierarchies
  7. Prefer composition over inheritance - Go philosophy
  8. Test embedded behavior - Ensure methods work correctly

โŒ Anti-Patterns

// โŒ Bad: Deep embedding hierarchy
type A struct{}
type B struct{ A }
type C struct{ B }
type D struct{ C }

// โœ… Good: Flat composition
type D struct {
    a *A
    b *B
    c *C
}

// โŒ Bad: Embedding for inheritance
type Base struct {
    value int
}

type Derived struct {
    Base
}

// โœ… Good: Explicit composition
type Derived struct {
    base *Base
}

// โŒ Bad: Unclear method promotion
type Logger struct {
    Writer
}

// โœ… Good: Clear composition
type Logger struct {
    output Writer
}

Resources and References

Official Documentation

Tools and Resources

Summary

Interface composition and embedding enable powerful designs:

  • Combine interfaces for flexibility
  • Embed types for code reuse
  • Promote methods intentionally
  • Keep hierarchies shallow
  • Prefer composition over inheritance
  • Document embedded relationships
  • Test embedded behavior

Master composition for flexible Go designs.

Comments