Go 从 0 到精通 · 第 15 课:组合优于继承
学习定位:这是整套 Go 教程的第 15 课。
前置要求:已经完成第 14 课,掌握了接口的定义、实现、鸭子类型和空接口。
本课目标:理解 Go"组合优于继承"的设计哲学,掌握结构体嵌入实现组合、接口 + 组合的配合使用,能避免用传统 OOP 思维硬套 Go。
1. 本课你要解决的核心问题
在 Java 或 C++ 里,你会用继承来复用代码:
Dog extends Animal,自动拥有 Animal 的字段和方法。
但 Go 没有 extends,没有类继承。那 Go 怎么实现代码复用和层级关系?
答案是组合——把一个类型嵌入到另一个类型中,而不是继承它。
你需要搞明白以下问题:
- 为什么 Go 不用继承
- 嵌入(embedding)怎么实现组合
- 嵌入和继承有什么区别
- 接口 + 组合怎么配合使用
- 怎么避免用"继承思维"写 Go
学完这一课,你就能用 Go 的方式思考代码复用了。
2. 为什么 Go 没有继承
2.1 继承的问题
经典继承(class inheritance)有几个已知问题:
- 紧耦合:子类和父类深度绑定,改父类容易影响所有子类
- 脆弱基类问题:父类的改动可能导致子类行为异常
- 层次结构僵硬:多继承导致"菱形问题",单继承又不够灵活
- “is-a” 关系过度使用:很多被设计成继承的关系,其实是"has-a"或"can-do"
2.2 Go 的选择
Go 选择了两个工具来替代继承:
- 组合(composition):用结构体嵌入来复用数据和方法
- 接口(interface):用接口来表达行为抽象
组合复用实现,接口复用抽象。
3. 结构体嵌入:组合的核心
3.1 回顾:嵌入基础
第 12 课你学过匿名嵌套(嵌入):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| type Address struct { City string Street string }
type Student struct { Name string Age int Address }
func main() { s := Student{ Name: "小明", Age: 20, Address: Address{ City: "北京", Street: "中关村大街", }, } fmt.Println(s.City) }
|
嵌入让外层类型"拥有"了内层类型的字段。但嵌入还能做什么?
3.2 嵌入类型的方法也被"提升"
嵌入不仅提升字段,还提升方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| type Logger struct { Prefix string }
func (l *Logger) Log(message string) { fmt.Printf("[%s] %s\n", l.Prefix, message) }
type Server struct { Name string Logger }
func main() { s := Server{ Name: "WebServer", Logger: Logger{Prefix: "INFO"}, }
s.Log("服务器启动") }
|
Server 没有定义 Log 方法,但因为嵌入了 Logger,Logger 的方法被提升到了 Server 上。
这就像继承中的"子类拥有父类方法",但本质上完全不同。
3.3 嵌入 vs 继承
| 特性 |
继承 |
嵌入 |
| 关系 |
is-a(Dog is Animal) |
has-a(Server has Logger) |
| 耦合 |
紧密绑定 |
松散组合 |
| 修改父类影响 |
所有子类 |
不影响外层 |
| 方法覆盖 |
override |
外层同名方法优先 |
4. 组合的实战模式
4.1 能力复用:把通用功能嵌入到多个类型中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package main
import "fmt"
type Identifiable struct { ID int }
func (i *Identifiable) SetID(id int) { i.ID = id }
func (i Identifiable) GetID() int { return i.ID }
type User struct { Name string Identifiable }
type Order struct { Product string Identifiable }
func main() { u := User{Name: "小明"} u.SetID(1001) fmt.Printf("用户: %s, ID: %d\n", u.Name, u.GetID())
o := Order{Product: "键盘"} o.SetID(2001) fmt.Printf("订单: %s, ID: %d\n", o.Product, o.GetID()) }
|
Identifiable 是一个通用能力,任何需要 ID 管理的类型都可以嵌入它。
4.2 多层嵌入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| type Engine struct { Power int }
func (e Engine) Start() { fmt.Printf("引擎启动,功率: %d 马力\n", e.Power) }
type Car struct { Brand string Engine }
type Truck struct { LoadCapacity float64 Engine }
func main() { car := Car{Brand: "Tesla", Engine: Engine{Power: 300}} car.Start()
truck := Truck{LoadCapacity: 10, Engine: Engine{Power: 500}} truck.Start() }
|
Car 和 Truck 都嵌入了 Engine,共享启动行为,但它们之间没有继承关系。
4.3 组合多个能力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type Logger struct{} func (Logger) Log(msg string) { fmt.Println("[LOG]", msg) }
type Config struct{} func (Config) GetConfig(key string) string { return "config_value" }
type Server struct { Name string Logger Config }
func main() { s := Server{Name: "API"} s.Log("启动中") fmt.Println(s.GetConfig("port")) }
|
一个类型可以嵌入多个其他类型,获得多种能力。这比多继承灵活得多——不存在菱形问题。
5. 接口 + 组合
组合和接口配合使用,是 Go 最强大的设计模式。
5.1 场景:通用缓存系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package main
import "fmt"
type Storage interface { Save(key string, value string) Load(key string) (string, bool) }
type MemoryStorage struct { data map[string]string }
func NewMemoryStorage() *MemoryStorage { return &MemoryStorage{data: make(map[string]string)} }
func (m *MemoryStorage) Save(key string, value string) { m.data[key] = value }
func (m *MemoryStorage) Load(key string) (string, bool) { v, ok := m.data[key] return v, ok }
type UserService struct { storage Storage }
func NewUserService(s Storage) *UserService { return &UserService{storage: s} }
func (us *UserService) Register(name string) { us.storage.Save("user:"+name, name) fmt.Println("注册用户:", name) }
func (us *UserService) Find(name string) { if v, ok := us.storage.Load("user:" + name); ok { fmt.Println("找到用户:", v) } else { fmt.Println("用户不存在:", name) } }
func main() { store := NewMemoryStorage() svc := NewUserService(store)
svc.Register("小明") svc.Register("小红") svc.Find("小明") svc.Find("小王") }
|
UserService 不关心存储的具体实现——可以是内存、文件、数据库。只要满足 Storage 接口就行。
这就是"接收接口,返回结构体"的实践。
5.2 接口组合
接口也可以嵌入其他接口:
1 2 3 4 5 6 7 8 9 10 11 12
| type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface { Reader Writer }
|
ReadWriter 等价于同时有 Read 和 Write 方法。Go 标准库大量使用这种模式。
6. 对比:继承 vs 组合 + 接口
假设要实现一个动物系统:
继承方式(Java 风格)
1 2 3 4 5 6 7
| Animal (abstract) ├── name, age 字段 ├── abstract speak() 方法 ├── Dog extends Animal │ └── speak() → "汪汪" └── Cat extends Animal └── speak() → "喵喵"
|
Go 的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| type Speaker interface { Speak() string }
type Dog struct { Name string Age int }
func (d Dog) Speak() string { return "汪汪" }
type Cat struct { Name string Age int }
func (c Cat) Speak() string { return "喵喵" }
type AnimalInfo struct { Name string Age int }
type Dog2 struct { AnimalInfo Breed string }
type Cat2 struct { AnimalInfo Indoor bool }
|
Go 没有 Animal 基类——接口定义行为,组合共享数据。
7. 一段综合示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| package main
import "fmt"
type Logger struct { Component string }
func (l *Logger) Info(msg string) { fmt.Printf("[INFO][%s] %s\n", l.Component, msg) }
func (l *Logger) Error(msg string) { fmt.Printf("[ERROR][%s] %s\n", l.Component, msg) }
type Identifiable struct { ID int }
func (i *Identifiable) SetID(id int) { i.ID = id }
type Storable interface { Key() string Value() string }
type User struct { Name string Logger Identifiable }
func (u User) Key() string { return fmt.Sprintf("user:%d", u.ID) }
func (u User) Value() string { return u.Name }
type Product struct { Title string Price float64 Logger Identifiable }
func (p Product) Key() string { return fmt.Sprintf("product:%d", p.ID) }
func (p Product) Value() string { return fmt.Sprintf("%s (¥%.2f)", p.Title, p.Price) }
type Store struct { data map[string]string }
func NewStore() *Store { return &Store{data: make(map[string]string)} }
func (s *Store) Save(item Storable) { s.data[item.Key()] = item.Value() }
func (s *Store) PrintAll() { fmt.Println("=== 存储内容 ===") for k, v := range s.data { fmt.Printf(" %s → %s\n", k, v) } }
func main() { store := NewStore()
u := User{ Name: "小明", Logger: Logger{Component: "User"}, Identifiable: Identifiable{ID: 101}, } u.Info("创建用户") store.Save(u)
p := Product{ Title: "机械键盘", Price: 299.0, Logger: Logger{Component: "Product"}, Identifiable: Identifiable{ID: 201}, } p.Info("创建商品") store.Save(p)
store.PrintAll() }
|
输出:
1 2 3 4 5
| [INFO][User] 创建用户 [INFO][Product] 创建商品 === 存储内容 === user:101 → 小明 product:201 → 机械键盘 (¥299.00)
|
这个示例展示了:
Logger 嵌入给 User 和 Product 都提供了日志能力
Identifiable 嵌入给它们都提供了 ID 管理能力
Storable 接口让 Store 能存储任何满足条件的类型
- 组合 + 接口 = 灵活、解耦的设计
8. 常见坑总结
8.1 把嵌入当继承用
1 2 3 4 5 6 7 8 9 10 11 12 13
| type Animal struct { Name string }
type Dog struct { Animal }
func main() { d := Dog{} }
|
嵌入不等于继承。Dog 不是 Animal 的子类型。
8.2 嵌入导致字段名冲突
1 2 3 4 5 6 7
| type A struct { Name string } type B struct { Name string } type C struct { A B }
|
多个嵌入有同名字段或方法时,必须用完整路径:c.A.Name。
8.3 过度嵌入
1 2 3 4 5 6 7 8
| type Server struct { Logger Config Cache Database Metrics Auth }
|
嵌入太多会让类型臃肿、职责不清。保持简单。
8.4 以为嵌入是"has-a"的唯一方式
嵌入是匿名字段的语法糖。如果需要明确的关系,用命名字段更好:
1 2 3 4 5
| type Server struct { logger Logger config Config }
|
嵌入适合"这个类型是这个类型的组成部分"的场景。
9. 本课练习
练习 1:能力复用
要求:
- 定义
Timestamped 结构体(含 CreatedAt string 字段和 SetNow() 方法)
- 定义
Post 和 Comment 结构体,都嵌入 Timestamped
- 验证两者都能调用
SetNow()
练习 2:接口 + 组合
要求:
- 定义
Notifiable 接口(Send(message string) 方法)
- 实现
EmailNotifier 和 SMSNotifier
- 定义
AlertService,接收 Notifiable 接口
- 分别用 Email 和 SMS 方式发送告警
练习 3:替代继承
要求:
- 用嵌入模拟以下关系:
Vehicle → Car、Truck
Vehicle 有 Speed 字段和 Accelerate() 方法
Car 额外有 Passengers int,Truck 额外有 CargoWeight float64
- 验证
Car 和 Truck 都能调用 Accelerate()
练习 4:接口组合
要求:
- 定义
Reader、Writer、Closer 三个小接口
- 定义
ReadWriterCloser 接口,嵌入上面三个
- 实现一个
Buffer 类型满足 ReadWriterCloser
10. 自测题
10.1 概念题
- Go 为什么没有继承?
- 嵌入和继承的核心区别是什么?
- 嵌入类型的方法怎么被外层类型调用?
- 一个类型可以嵌入多个其他类型吗?
- "接收接口,返回结构体"是什么意思?
- 接口可以嵌入其他接口吗?
- 嵌入和命名字段有什么区别?
- 什么时候用命名字段,什么时候用嵌入?
11. 本课总结
这一课你学到了 Go 的核心设计哲学——组合优于继承。
你现在应该已经理解:
- Go 用组合(嵌入)和接口代替继承
- 嵌入让外层类型获得内层类型的字段和方法
- 接口定义行为,组合共享实现
- 接口 + 组合可以实现灵活、解耦的设计
- 不要过度嵌入,保持类型职责清晰
- 嵌入不等于继承——
Dog 嵌入 Animal 不意味着 Dog is Animal
12. 下一课预告
下一课我们学习 Go 中非常重要的主题:错误处理 error。
会重点讲:
error 接口是什么
- 怎么创建和返回错误
errors.New 和 fmt.Errorf 的区别
- 错误处理的最佳实践
学完下一课,你就能写出清晰可靠的错误处理逻辑了。