Keep the Promise
The Liskov Substitution Principle says: anywhere the code expects an interface, every implementation of that interface must work correctly. An implementation that technically fits but behaves badly — panics, does nothing, lies — breaks every function that trusted the interface.
An interface is a promise. Fly() promises flying. A type that implements Fly() by crashing has signed a contract it cannot keep.
A Substitute That Breaks the Deal
The Bird interface promises Fly(). Sparrows deliver. Penguins are birds too — so it feels natural to implement the interface... with a panic, because penguins cannot fly.
Now airShow, which works for every honest bird, crashes on a penguin. The bug is not in airShow — it is in the substitute that broke the promise.
The penguin that crashes the show
airShow trusts the interface and calls Fly() on every bird. The sparrow performs; the penguin panics and takes the whole program down (the recover only softens the landing to show the message).
Output: Go program and captured output
package main
import "fmt"
type Bird interface {
Fly() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "sparrow: flying low"
}
// Penguin signs the contract — but cannot keep it.
type Penguin struct{}
func (p Penguin) Fly() string {
panic("penguins cannot fly")
}
func airShow(birds []Bird) {
for _, b := range birds {
fmt.Println(b.Fly())
}
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("the show crashed:", r)
}
}()
airShow([]Bird{Sparrow{}, Penguin{}})
}
Separate the Promises
The fix: promise only what is true. Split the fat promise into Flyer and Swimmer. The sparrow implements Flyer, the penguin implements Swimmer — and no type signs a contract it cannot keep.
The best part is who catches the mistake now: passing a penguin to airShow is no longer a crash at runtime — it is a compile error. The program will not even build.
Every type keeps its promise
airShow accepts Flyer, poolShow accepts Swimmer. Each animal appears only where its promise holds — substitution is safe by construction.
Output: Go program and captured output
package main
import "fmt"
type Flyer interface {
Fly() string
}
type Swimmer interface {
Swim() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "sparrow: flying low"
}
type Penguin struct{}
func (p Penguin) Swim() string {
return "penguin: diving deep"
}
func airShow(flyers []Flyer) {
for _, f := range flyers {
fmt.Println(f.Fly())
}
}
func poolShow(swimmers []Swimmer) {
for _, s := range swimmers {
fmt.Println(s.Swim())
}
}
func main() {
airShow([]Flyer{Sparrow{}})
poolShow([]Swimmer{Penguin{}})
// airShow([]Flyer{Penguin{}}) would not even compile.
}
Any Flyer can join
The payoff of honest interfaces: a new Eagle joins the air show with no risk — anything that implements Flyer truly flies, so airShow never needs to doubt its input.
Output: Go program and captured output
package main
import "fmt"
type Flyer interface {
Fly() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "sparrow: flying low"
}
// Eagle is new — and it truly flies, so it substitutes safely.
type Eagle struct{}
func (e Eagle) Fly() string {
return "eagle: soaring high"
}
func airShow(flyers []Flyer) {
for _, f := range flyers {
fmt.Println(f.Fly())
}
}
func main() {
airShow([]Flyer{Sparrow{}, Eagle{}})
}