Go Interfaces
Master Go's interface system for creating flexible, decoupled code through implicit implementation and composition patterns.
Basic Interfaces
Defining and implementing interfaces:
package main
import "fmt"
// Define interface type Writer interface { Write(p []byte) (n int, err error) }
// Implement interface (implicit) type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(p []byte) (n int, err error) { fmt.Print(string(p)) return len(p), nil }
func main() { var w Writer = ConsoleWriter{} w.Write([]byte("Hello, World!\n")) }
Multiple methods in interface:
type Reader interface { Read(p []byte) (n int, err error) }
type ReadWriter interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) }
// Implement ReadWriter type File struct { name string }
func (f *File) Read(p []byte) (n int, err error) { // Implementation return 0, nil }
func (f *File) Write(p []byte) (n int, err error) { // Implementation return len(p), nil }
Empty Interface
Using interface{} (any in Go 1.18+):
// Accepts any type func printValue(v interface{}) { fmt.Println(v) }
// Modern syntax (Go 1.18+) func printAny(v any) { fmt.Println(v) }
func main() { printValue(42) printValue("hello") printValue(true)
printAny(3.14)
}
Type assertions:
func processValue(v interface{}) { // Type assertion if str, ok := v.(string); ok { fmt.Println("String:", str) }
// Type switch
switch val := v.(type) {
case int:
fmt.Println("Integer:", val)
case string:
fmt.Println("String:", val)
case bool:
fmt.Println("Boolean:", val)
default:
fmt.Println("Unknown type")
}
}
Interface Composition
Embedding interfaces:
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 }
// Standard library example import "io"
func useReadWriteCloser(rwc io.ReadWriteCloser) { // Can call Read, Write, and Close rwc.Write([]byte("data")) rwc.Close() }
Common Interfaces
Standard library interfaces:
// Stringer interface type Stringer interface { String() string }
type Person struct { Name string Age int }
func (p Person) String() string { return fmt.Sprintf("%s (%d years old)", p.Name, p.Age) }
// error interface type error interface { Error() string }
type MyError struct { Message string }
func (e MyError) Error() string { return e.Message }
// sort.Interface type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) }
type ByAge []Person
func (a ByAge) Len() int { return len(a) } func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
Interface Design Patterns
Small interfaces:
// Good: small, focused interfaces type Getter interface { Get(key string) (value string, exists bool) }
type Setter interface { Set(key, value string) }
type Deleter interface { Delete(key string) }
// Compose as needed type Cache interface { Getter Setter Deleter }
Accept interfaces, return structs:
// Accept interface parameter func processReader(r io.Reader) error { data, err := io.ReadAll(r) if err != nil { return err } fmt.Println(string(data)) return nil }
// Return concrete type func newConfig() *Config { return &Config{ Host: "localhost", Port: 8080, } }
type Config struct { Host string Port int }
Nil Interfaces
Understanding nil interfaces:
func checkNil() { var i interface{} fmt.Println(i == nil) // true
var p *Person
i = p
fmt.Println(i == nil) // false! (type is set, value is nil)
// Proper nil check
v, ok := i.(*Person)
fmt.Println(v == nil, ok) // true, true
}
Interface Satisfaction
Checking interface implementation:
// Compile-time check var _ io.Writer = (*MyWriter)(nil) var _ io.Reader = (*MyReader)(nil)
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (n int, err error) { return len(p), nil }
// If MyWriter doesn't implement Writer, compilation fails
Duck Typing
Implicit interface satisfaction:
// No explicit "implements" keyword needed type Duck interface { Quack() Walk() }
type RealDuck struct{}
func (d RealDuck) Quack() { fmt.Println("Quack!") }
func (d RealDuck) Walk() { fmt.Println("Waddle waddle") }
type Robot struct{}
func (r Robot) Quack() { fmt.Println("Beep boop quack") }
func (r Robot) Walk() { fmt.Println("mechanical walking sounds") }
func makeDuckDoThings(d Duck) { d.Quack() d.Walk() }
func main() { makeDuckDoThings(RealDuck{}) makeDuckDoThings(Robot{}) }
Polymorphism
Using interfaces for polymorphism:
type Shape interface { Area() float64 Perimeter() float64 }
type Rectangle struct { Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Circle struct { Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * 3.14159 * c.Radius }
func printShapeInfo(s Shape) { fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter()) }
func main() { shapes := []Shape{ Rectangle{Width: 10, Height: 5}, Circle{Radius: 7}, }
for _, shape := range shapes {
printShapeInfo(shape)
}
}
Dependency Injection
Using interfaces for testability:
// Define interface for dependency type UserRepository interface { GetUser(id int) (*User, error) SaveUser(user *User) error }
// Production implementation type PostgresUserRepo struct { db *sql.DB }
func (r *PostgresUserRepo) GetUser(id int) (*User, error) { // Database query return &User{}, nil }
func (r *PostgresUserRepo) SaveUser(user *User) error { // Database insert/update return nil }
// Test implementation type MockUserRepo struct { users map[int]*User }
func (m *MockUserRepo) GetUser(id int) (*User, error) { user, exists := m.users[id] if !exists { return nil, errors.New("user not found") } return user, nil }
func (m *MockUserRepo) SaveUser(user *User) error { m.users[user.ID] = user return nil }
// Service depends on interface, not concrete type type UserService struct { repo UserRepository }
func (s *UserService) GetUserName(id int) (string, error) { user, err := s.repo.GetUser(id) if err != nil { return "", err } return user.Name, nil }
type User struct { ID int Name string }
Builder Pattern with Interfaces
Fluent interface pattern:
type QueryBuilder interface { Select(fields ...string) QueryBuilder From(table string) QueryBuilder Where(condition string) QueryBuilder Build() string }
type sqlQueryBuilder struct { selectFields []string fromTable string whereClause string }
func NewQueryBuilder() QueryBuilder { return &sqlQueryBuilder{} }
func (b *sqlQueryBuilder) Select(fields ...string) QueryBuilder { b.selectFields = fields return b }
func (b *sqlQueryBuilder) From(table string) QueryBuilder { b.fromTable = table return b }
func (b *sqlQueryBuilder) Where(condition string) QueryBuilder { b.whereClause = condition return b }
func (b *sqlQueryBuilder) Build() string { query := "SELECT " + strings.Join(b.selectFields, ", ") query += " FROM " + b.fromTable if b.whereClause != "" { query += " WHERE " + b.whereClause } return query }
func main() { query := NewQueryBuilder(). Select("id", "name", "email"). From("users"). Where("age > 18"). Build()
fmt.Println(query)
}
Strategy Pattern
Implementing strategy pattern:
type PaymentStrategy interface { Pay(amount float64) error }
type CreditCardPayment struct { CardNumber string }
func (c *CreditCardPayment) Pay(amount float64) error { fmt.Printf("Paying %.2f with credit card %s\n", amount, c.CardNumber) return nil }
type PayPalPayment struct { Email string }
func (p *PayPalPayment) Pay(amount float64) error { fmt.Printf("Paying %.2f via PayPal to %s\n", amount, p.Email) return nil }
type ShoppingCart struct { paymentMethod PaymentStrategy }
func (cart *ShoppingCart) SetPaymentMethod(pm PaymentStrategy) { cart.paymentMethod = pm }
func (cart *ShoppingCart) Checkout(amount float64) error { return cart.paymentMethod.Pay(amount) }
func main() { cart := &ShoppingCart{}
cart.SetPaymentMethod(&CreditCardPayment{CardNumber: "1234-5678"})
cart.Checkout(100.00)
cart.SetPaymentMethod(&PayPalPayment{Email: "user@example.com"})
cart.Checkout(50.00)
}
Adapter Pattern
Adapting interfaces:
// Third-party logger type ThirdPartyLogger struct{}
func (t *ThirdPartyLogger) LogMessage(msg string, level int) { fmt.Printf("[Level %d] %s\n", level, msg) }
// Our application interface type Logger interface { Info(msg string) Error(msg string) }
// Adapter type LoggerAdapter struct { thirdParty *ThirdPartyLogger }
func (a *LoggerAdapter) Info(msg string) { a.thirdParty.LogMessage(msg, 0) }
func (a *LoggerAdapter) Error(msg string) { a.thirdParty.LogMessage(msg, 2) }
func useLogger(logger Logger) { logger.Info("Application started") logger.Error("An error occurred") }
func main() { adapter := &LoggerAdapter{ thirdParty: &ThirdPartyLogger{}, } useLogger(adapter) }
When to Use This Skill
Use go-interfaces when you need to:
-
Define contracts for behavior without implementation
-
Enable polymorphism and code reuse
-
Create testable code with dependency injection
-
Implement design patterns (strategy, adapter, etc.)
-
Build plugin systems or extensible architectures
-
Decouple components in large applications
-
Mock dependencies in tests
-
Follow SOLID principles in Go
-
Create flexible, maintainable APIs
-
Support multiple implementations of same behavior
Best Practices
-
Keep interfaces small and focused (1-3 methods)
-
Accept interfaces, return concrete types
-
Define interfaces where they're used, not implemented
-
Use interface composition for complex interfaces
-
Don't use empty interface unless absolutely necessary
-
Verify interface implementation at compile time
-
Document expected behavior in interface comments
-
Prefer many small interfaces over large ones
-
Use standard library interfaces when applicable
-
Name interfaces with -er suffix (Reader, Writer, etc.)
Common Pitfalls
-
Making interfaces too large or generic
-
Defining unused interfaces "just in case"
-
Returning interfaces instead of concrete types
-
Not checking for nil interface values properly
-
Over-abstracting simple code
-
Forgetting that interfaces are satisfied implicitly
-
Using empty interface excessively
-
Not documenting interface contracts
-
Creating interfaces for single implementation
-
Confusing nil value vs nil interface
Resources
-
Effective Go - Interfaces
-
Go Blog - Laws of Reflection
-
Interface Documentation
-
Go by Example - Interfaces
-
Accept Interfaces, Return Structs