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
- Use embedding for composition - Avoid deep inheritance
- Embed interfaces for flexibility - Combine small interfaces
- Promote methods intentionally - Clear what’s inherited
- Document embedded types - Explain the relationship
- Use embedding for code reuse - Share common behavior
- Keep embedding shallow - Avoid complex hierarchies
- Prefer composition over inheritance - Go philosophy
- 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
- Effective Go - Embedding - Official guide
- Interface Documentation - Language specification
- Composition vs Inheritance - Design philosophy
Recommended Reading
- Go Code Review Comments - Code review guide
- Embedding in Go - Detailed explanation
- Interface Segregation Principle - Design principle
Tools and Resources
- Go Playground - Online Go editor
- pkg.go.dev - Package documentation
- Go Code Review Comments - Best practices
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