Understanding Interfaces
Duck Typing and Implicit Satisfaction
Go's interfaces are satisfied implicitly - there's no "implements" keyword. If a type has all the methods an interface requires, it satisfies that interface. This is known as structural typing or duck typing: "If it walks like a duck and quacks like a duck, it's a duck."
Interface Principles
- Implicit satisfaction: No explicit declaration needed
- Small interfaces: The bigger the interface, the weaker the abstraction
- Accept interfaces, return structs: Maximize flexibility
- Interface segregation: Many small interfaces > one large interface
Interface Internals
An interface value consists of two components: a type and a value. Understanding this structure is crucial for avoiding nil interface gotchas and properly using type assertions.
// Interface internals (conceptual) type iface struct { tab *itab // Type information data unsafe.Pointer // Pointer to actual data } type itab struct { inter *interfacetype // Interface type _type *_type // Concrete type fun [1]uintptr // Method table }
Interface Basics
Defining and Implementing Interfaces
Interfaces define contracts that types can satisfy. They specify what methods a type must have but not how those methods are implemented.
// Interface definition type Writer interface { Write([]byte) (int, error) } type Reader interface { Read([]byte) (int, error) } // Composite interface type ReadWriter interface { Reader Writer } // Custom implementation type MyWriter struct { data []byte } func (w *MyWriter) Write(p []byte) (int, error) { w.data = append(w.data, p...) return len(p), nil } // MyWriter now implements Writer interface var w Writer = &MyWriter{} // Multiple interface satisfaction type Buffer struct { data []byte } func (b *Buffer) Read(p []byte) (int, error) { n := copy(p, b.data) b.data = b.data[n:] return n, nil } func (b *Buffer) Write(p []byte) (int, error) { b.data = append(b.data, p...) return len(p), nil } // Buffer implements both Reader and Writer var rw ReadWriter = &Buffer{}
The Empty Interface
The empty interface interface{}
(or any
in Go 1.18+) is satisfied
by all types. It's Go's way of representing "any type" but should be used sparingly.
⚠️ Empty Interface Cautions
- Loss of type safety - requires type assertions
- Runtime panics possible with incorrect assertions
- Makes code harder to understand and maintain
- Use only when truly necessary (e.g., JSON unmarshaling)
// Empty interface usage func PrintAnything(v interface{}) { fmt.Printf("Type: %T, Value: %v\n", v, v) } PrintAnything(42) // int PrintAnything("hello") // string PrintAnything([]int{1,2,3}) // slice // Container for any type type Container struct { items []interface{} } func (c *Container) Add(item interface{}) { c.items = append(c.items, item) } // Go 1.18+ alias type any = interface{} func Process(data any) { // Process any type }
Type Assertions and Type Switches
Type Assertions
Type assertions extract the concrete value from an interface. They can be used with a single return value (panics on failure) or two return values (safe).
// Type assertion basics var i interface{} = "hello" // Unsafe assertion (panics if wrong type) s := i.(string) fmt.Println(s) // "hello" // Safe assertion with ok check s, ok := i.(string) if ok { fmt.Printf("String: %s\n", s) } else { fmt.Println("Not a string") } // Failed assertion n, ok := i.(int) if !ok { fmt.Println("Not an int") // This executes } // Asserting to interface type var w io.Writer = &bytes.Buffer{} rw, ok := w.(io.ReadWriter) if ok { fmt.Println("Also implements Reader") }
Type Switches
Type switches are like regular switches but operate on types rather than values. They're cleaner than multiple type assertions for handling multiple possible types.
// Type switch example func describe(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Integer: %d\n", v) case string: fmt.Printf("String: %q\n", v) case bool: fmt.Printf("Boolean: %t\n", v) case []int: fmt.Printf("Slice of ints: %v\n", v) case nil: fmt.Println("nil value") default: fmt.Printf("Unknown type: %T\n", v) } } // Multiple types in case func process(i interface{}) { switch i.(type) { case int, int32, int64: fmt.Println("Integer type") case float32, float64: fmt.Println("Float type") case string: fmt.Println("String type") } }
Common Interface Patterns
Standard Library Interfaces
Go's standard library defines many small, focused interfaces. Understanding and using these interfaces makes your code more idiomatic and interoperable.
// io.Reader - fundamental input interface type Reader interface { Read(p []byte) (n int, err error) } // io.Writer - fundamental output interface type Writer interface { Write(p []byte) (n int, err error) } // io.Closer - resource cleanup type Closer interface { Close() error } // fmt.Stringer - custom string representation type Stringer interface { String() string } // error - error handling type error interface { Error() string } // sort.Interface - sorting collections type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } // Custom type implementing multiple interfaces type LogFile struct { file *os.File } func (l *LogFile) Write(p []byte) (int, error) { return l.file.Write(p) } func (l *LogFile) Close() error { return l.file.Close() } func (l *LogFile) String() string { return l.file.Name() } // LogFile implements Writer, Closer, and Stringer
Interface Composition
Interfaces can embed other interfaces, creating larger interfaces from smaller ones. This promotes interface segregation and reusability.
// Composing interfaces type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } type ReadWriteSeeker interface { Reader Writer Seeker } // Custom composite interfaces type Storage interface { Save(key string, data []byte) error Load(key string) ([]byte, error) } type CachedStorage interface { Storage InvalidateCache(key string) ClearCache() }
Design Patterns with Interfaces
Strategy Pattern
The strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable through interfaces.
// Strategy pattern type PaymentStrategy interface { Pay(amount float64) error ValidateDetails() error } type CreditCard struct { Number string CVV string } func (c *CreditCard) Pay(amount float64) error { fmt.Printf("Paying $%.2f with credit card\n", amount) return nil } func (c *CreditCard) ValidateDetails() error { if len(c.Number) != 16 { return fmt.Errorf("invalid card number") } return nil } type PayPal struct { Email string } func (p *PayPal) Pay(amount float64) error { fmt.Printf("Paying $%.2f via PayPal\n", amount) return nil } func (p *PayPal) ValidateDetails() error { if !strings.Contains(p.Email, "@") { return fmt.Errorf("invalid email") } return nil } // Context using strategy type PaymentProcessor struct { strategy PaymentStrategy } func (p *PaymentProcessor) ProcessPayment(amount float64) error { if err := p.strategy.ValidateDetails(); err != nil { return err } return p.strategy.Pay(amount) }
Dependency Injection
Interfaces enable dependency injection, making code more testable and flexible by depending on abstractions rather than concrete implementations.
// Dependency injection with interfaces type Logger interface { Log(message string) } type Database interface { Query(query string) ([]Row, error) Execute(cmd string) error } type Service struct { logger Logger db Database } func NewService(logger Logger, db Database) *Service { return &Service{ logger: logger, db: db, } } func (s *Service) ProcessOrder(orderID string) error { s.logger.Log("Processing order: " + orderID) rows, err := s.db.Query("SELECT * FROM orders WHERE id = " + orderID) if err != nil { s.logger.Log("Error querying database: " + err.Error()) return err } // Process rows... return nil } // Testing with mocks type MockLogger struct { messages []string } func (m *MockLogger) Log(message string) { m.messages = append(m.messages, message) } type MockDatabase struct { queryFunc func(string) ([]Row, error) } func (m *MockDatabase) Query(query string) ([]Row, error) { return m.queryFunc(query) } func (m *MockDatabase) Execute(cmd string) error { return nil }
Interface Best Practices
Interface Design Guidelines
- Keep interfaces small: 1-3 methods is ideal
- Define interfaces where used: Not where implemented
- Accept interfaces, return structs: Maximize flexibility
- Don't export interfaces prematurely: Start concrete, abstract when needed
- Name interfaces with -er suffix: Reader, Writer, Closer
- Document interface contracts: Behavior expectations
Pattern | Good | Bad | Reason |
---|---|---|---|
Interface Size | 1-3 methods | 10+ methods | Smaller interfaces are more flexible |
Definition Location | Consumer package | Producer package | Consumer knows what it needs |
Parameter Type | Interface | Concrete type | Accept the minimum required |
Return Type | Concrete type | Interface | Return the maximum information |
Naming | Reader, Stringer | IReader, ReaderInterface | Go convention avoids prefixes/suffixes |
Common Pitfalls
Nil Interface Values
A common gotcha is that an interface value that holds a nil concrete value is itself non-nil. This can lead to unexpected behavior.
⚠️ Interface Gotchas
- Nil interface != nil concrete: Interface with nil pointer is not nil
- Type assertion panics: Always use two-value form for safety
- Interface comparison: Two interfaces are equal if types and values match
- Pointer vs value receivers: Affects interface satisfaction
// Nil interface gotcha type MyError struct { Message string } func (e *MyError) Error() string { if e == nil { return "" } return e.Message } func riskyFunction() error { var e *MyError = nil return e // Returns non-nil interface! } func main() { err := riskyFunction() if err != nil { fmt.Println("Got error:", err) // This executes! } } // Correct approach func safeFunction() error { var e *MyError = nil if e == nil { return nil // Return nil interface } return e } // Interface comparison var a, b interface{} a = 42 b = 42 fmt.Println(a == b) // true a = []int{1} b = []int{1} // fmt.Println(a == b) // Panic! Slices aren't comparable
🎯 Practice Exercises
Exercise 1: Plugin System
Design a plugin system using interfaces that allows dynamic loading of different processors.
Exercise 2: Mock Testing Framework
Create a simple mocking framework using interfaces for unit testing.
Exercise 3: Adapter Pattern
Implement adapters to make incompatible interfaces work together.
Exercise 4: Pipeline Processing
Build a data pipeline using interfaces for transformers and filters.