Interface Composition and Best Practices
Small interfaces and dependency inversion
Interface Composition and Best Practices is a free Go Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Go Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Interface Composition
Go interfaces can embed other interfaces to compose larger ones:
package main
import "io"
// io.ReadWriter composes Reader and Writer
type ReadWriter interface {
io.Reader
io.Writer
}
// Your own composition:
type Processor interface {
io.Reader
Process() error
io.Closer
}
func main() { _ = (*Processor)(nil) }Small Interface Principle
The most powerful Go interfaces have one or two methods. This maximizes the number of types that can satisfy them:
io.Reader— 1 methodio.Writer— 1 methoderror— 1 methodfmt.Stringer— 1 method
Large interfaces are harder to mock and test.
Dependency Inversion
Accept interfaces in function parameters rather than concrete types. This decouples your code from specific implementations:
package main
import "fmt"
type Store interface {
Get(key string) (string, bool)
Set(key, val string)
}
func cache(s Store, key, value string) {
if _, ok := s.Get(key); !ok {
s.Set(key, value)
}
}
type MapStore struct{ m map[string]string }
func (ms *MapStore) Get(k string) (string, bool) { v, ok := ms.m[k]; return v, ok }
func (ms *MapStore) Set(k, v string) { ms.m[k] = v }
func main() {
s := &MapStore{m: map[string]string{}}
cache(s, "x", "hello")
v, _ := s.Get("x")
fmt.Println(v)
}Interface Segregation
Split large interfaces into smaller, focused ones. Clients only depend on the methods they use:
package main
// Bad: large interface forces all methods
type BigStorage interface {
Read(key string) string
Write(key, val string)
Delete(key string)
List() []string
Backup() error
}
// Good: small, focused interfaces
type Reader interface{ Read(key string) string }
type Writer interface{ Write(key, val string) }
type Lister interface{ List() []string }
func readAll(r Reader) {} // only needs Read
func main() { _ = readAll }Returning Concrete Types
Constructors should return concrete types, not interfaces. Callers can always assign a concrete type to an interface variable:
package main
type Logger interface{ Log(string) }
type ConsoleLogger struct{}
func (c *ConsoleLogger) Log(s string) {}
// Good: return concrete *ConsoleLogger
func NewConsoleLogger() *ConsoleLogger { return &ConsoleLogger{} }
// Avoid: return Logger (hides the concrete type)
// func NewConsoleLogger() Logger { return &ConsoleLogger{} }
func main() { _ = NewConsoleLogger() }Interface Pollution
Don't create an interface for every type "just in case". Create interfaces only when:
- You have multiple implementations now or soon
- You need to mock the dependency in tests
- You are building a library and want to decouple callers
Embedding Interfaces for Extension
Embed an interface to add optional methods with defaults:
package main
import "fmt"
type Handler interface{ Handle(msg string) }
type NamedHandler interface {
Handler
Name() string
}
func dispatch(h Handler) {
if nh, ok := h.(NamedHandler); ok {
fmt.Printf("[%s] ", nh.Name())
}
h.Handle("event")
}
type MyHandler struct{}
func (m MyHandler) Handle(msg string) { fmt.Println(msg) }
func (m MyHandler) Name() string { return "my" }
func main() { dispatch(MyHandler{}) }Mocking with Interfaces
Define dependencies as interfaces; inject mocks in tests:
package main
import "fmt"
type Clock interface{ Now() int64 }
type MockClock struct{ T int64 }
func (m MockClock) Now() int64 { return m.T }
func isExpired(c Clock, expiresAt int64) bool {
return c.Now() > expiresAt
}
func main() {
mc := MockClock{T: 1000}
fmt.Println(isExpired(mc, 500)) // true
fmt.Println(isExpired(mc, 1500)) // false
}Interface vs Struct Embedding for Reuse
Use interface embedding for API composition and struct embedding for code reuse. They serve different purposes and should not be confused.
Detecting Interface Compliance at Compile Time
Use a blank identifier compile-time check to verify your type satisfies an interface:
package main
type Doer interface{ Do() }
type MyType struct{}
func (m *MyType) Do() {}
// Compile-time check — no runtime cost
var _ Doer = (*MyType)(nil)
func main() {}Quick Check
When should you define a new interface in Go?
Recap: Interface Best Practices
Key takeaways:
- Keep interfaces small — one or two methods
- Compose interfaces from smaller ones
- Accept interfaces, return concrete types
- Create interfaces for testability and multiple implementations
- Use blank-assignment compile-time check
Practice Prompt
Design a Notifier interface with Notify(msg string) error. Implement it for EmailNotifier and SMSNotifier. Write a broadcast function that sends to a []Notifier.
Frequently asked questions
Is the “Interface Composition and Best Practices” lesson free?
Yes — the full text of “Interface Composition and Best Practices” is free to read here on the web, and the Go Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Go Academy course, upgrade to CoddyKit PRO.
What will I learn in “Interface Composition and Best Practices”?
Small interfaces and dependency inversion You practise Go Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Go Academy?
No prior experience is required. Go Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Interface Composition and Best Practices” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Go Academy lesson?
Yes. Every Go Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Defining and Implementing Interfaces
- Core Standard Library Interfaces
- Type Assertions and Type Switches
- Interface Composition and Best Practices