Small Interfaces
The Interface Segregation Principle says: no type should be forced to implement methods it does not need. Instead of one fat interface with many methods, make several small ones — each type implements only what is true for it.
This is the most Go-native principle of the five: Go's standard library is built from tiny interfaces, most famously the one-method reader and writer that half the ecosystem plugs into.
The Fat Interface
A Worker interface with Work() and Eat() looks harmless — until a robot applies for the job. The robot works fine, but it must implement Eat() too, with... something. Nonsense answers like this are the signature of a fat interface.
The robot forced to eat
The robot implements Eat() only because the interface demands it. The lunch break prints an answer that should never exist — the type was forced to lie.
Output: Go program and captured output
package main
import "fmt"
// One fat interface — every worker MUST also eat.
type Worker interface {
Work() string
Eat() string
}
type Human struct{}
func (h Human) Work() string { return "human: working" }
func (h Human) Eat() string { return "human: eating lunch" }
type Robot struct{}
func (r Robot) Work() string { return "robot: working" }
// Forced by the interface — robots do not eat.
func (r Robot) Eat() string { return "robot: ERROR, cannot eat" }
func main() {
team := []Worker{Human{}, Robot{}}
for _, w := range team {
fmt.Println(w.Work())
}
for _, w := range team {
fmt.Println(w.Eat())
}
}
Split the Interface
Split the fat interface into Worker and Eater. The human implements both; the robot implements only Worker. The work shift takes workers, the lunch break takes eaters — the robot is simply never invited to lunch, and no method lies.
Each type implements only its truth
Two small interfaces instead of one fat one. The robot no longer has an Eat() method at all — the compiler keeps it out of the lunch break automatically.
Output: Go program and captured output
package main
import "fmt"
// Two small interfaces — each type implements only its truth.
type Worker interface {
Work() string
}
type Eater interface {
Eat() string
}
type Human struct{}
func (h Human) Work() string { return "human: working" }
func (h Human) Eat() string { return "human: eating lunch" }
type Robot struct{}
func (r Robot) Work() string { return "robot: working" }
func workShift(team []Worker) {
for _, w := range team {
fmt.Println(w.Work())
}
}
func lunchBreak(guests []Eater) {
for _, e := range guests {
fmt.Println(e.Eat())
}
}
func main() {
workShift([]Worker{Human{}, Robot{}})
lunchBreak([]Eater{Human{}})
}
Small Pieces Compose
Small interfaces are not a limitation — they combine. When some function really needs both abilities, it composes them into one interface on the spot: Worker plus Eater.
That is the standard-library pattern: define one-method interfaces, and let whoever needs more glue them together.
Compose when you need both
WorkingEater is built from the two small interfaces. The full-day shift needs both abilities, so only the human qualifies — said once, in the type system.
Output: Go program and captured output
package main
import "fmt"
type Worker interface {
Work() string
}
type Eater interface {
Eat() string
}
// Small interfaces compose into bigger ones when needed.
type WorkingEater interface {
Worker
Eater
}
type Human struct{}
func (h Human) Work() string { return "human: working" }
func (h Human) Eat() string { return "human: eating lunch" }
func fullDay(we WorkingEater) {
fmt.Println(we.Work())
fmt.Println(we.Eat())
fmt.Println(we.Work())
}
func main() {
fullDay(Human{})
}