Go 从 0 到精通 · 第 15 课:组合优于继承

学习定位:这是整套 Go 教程的第 15 课。
前置要求:已经完成第 14 课,掌握了接口的定义、实现、鸭子类型和空接口。
本课目标:理解 Go"组合优于继承"的设计哲学,掌握结构体嵌入实现组合、接口 + 组合的配合使用,能避免用传统 OOP 思维硬套 Go。


1. 本课你要解决的核心问题

在 Java 或 C++ 里,你会用继承来复用代码:

1
2
3
Animal
├── Dog
└── Cat

Dog extends Animal,自动拥有 Animal 的字段和方法。

但 Go 没有 extends,没有类继承。那 Go 怎么实现代码复用和层级关系?

答案是组合——把一个类型嵌入到另一个类型中,而不是继承它。

你需要搞明白以下问题:

  • 为什么 Go 不用继承
  • 嵌入(embedding)怎么实现组合
  • 嵌入和继承有什么区别
  • 接口 + 组合怎么配合使用
  • 怎么避免用"继承思维"写 Go

学完这一课,你就能用 Go 的方式思考代码复用了。


2. 为什么 Go 没有继承

2.1 继承的问题

经典继承(class inheritance)有几个已知问题:

  1. 紧耦合:子类和父类深度绑定,改父类容易影响所有子类
  2. 脆弱基类问题:父类的改动可能导致子类行为异常
  3. 层次结构僵硬:多继承导致"菱形问题",单继承又不够灵活
  4. “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 // 嵌入 Logger
}

func main() {
s := Server{
Name: "WebServer",
Logger: Logger{Prefix: "INFO"},
}

s.Log("服务器启动") // [INFO] 服务器启动
}

Server 没有定义 Log 方法,但因为嵌入了 LoggerLogger 的方法被提升到了 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"

// 通用的 ID 管理能力
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 // 嵌入,获得 ID 管理能力
}

// 订单
type Order struct {
Product string
Identifiable // 嵌入,获得 ID 管理能力
}

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 // 嵌入 Engine
}

type Truck struct {
LoadCapacity float64
Engine // 嵌入 Engine
}

func main() {
car := Car{Brand: "Tesla", Engine: Engine{Power: 300}}
car.Start() // 引擎启动,功率: 300 马力

truck := Truck{LoadCapacity: 10, Engine: Engine{Power: 500}}
truck.Start() // 引擎启动,功率: 500 马力
}

CarTruck 都嵌入了 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 // 获得 Log 能力
Config // 获得 GetConfig 能力
}

func main() {
s := Server{Name: "API"}
s.Log("启动中") // [LOG] 启动中
fmt.Println(s.GetConfig("port")) // config_value
}

一个类型可以嵌入多个其他类型,获得多种能力。这比多继承灵活得多——不存在菱形问题。


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
}

// 用户服务,依赖 Storage 接口
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 // 嵌入 Reader
Writer // 嵌入 Writer
}

ReadWriter 等价于同时有 ReadWrite 方法。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)
}

// ===== ID 管理能力 =====
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 嵌入给 UserProduct 都提供了日志能力
  • 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{}
// 以为可以:var a Animal = d ← 编译错误
// Dog 和 Animal 不是继承关系
}

嵌入不等于继承。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.Name → 编译错误:ambiguous selector

多个嵌入有同名字段或方法时,必须用完整路径: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
}
// s.logger.Log("msg") 需要明确调用

嵌入适合"这个类型是这个类型的组成部分"的场景。


9. 本课练习

练习 1:能力复用

要求:

  • 定义 Timestamped 结构体(含 CreatedAt string 字段和 SetNow() 方法)
  • 定义 PostComment 结构体,都嵌入 Timestamped
  • 验证两者都能调用 SetNow()

练习 2:接口 + 组合

要求:

  • 定义 Notifiable 接口(Send(message string) 方法)
  • 实现 EmailNotifierSMSNotifier
  • 定义 AlertService,接收 Notifiable 接口
  • 分别用 Email 和 SMS 方式发送告警

练习 3:替代继承

要求:

  • 用嵌入模拟以下关系:VehicleCarTruck
  • VehicleSpeed 字段和 Accelerate() 方法
  • Car 额外有 Passengers intTruck 额外有 CargoWeight float64
  • 验证 CarTruck 都能调用 Accelerate()

练习 4:接口组合

要求:

  • 定义 ReaderWriterCloser 三个小接口
  • 定义 ReadWriterCloser 接口,嵌入上面三个
  • 实现一个 Buffer 类型满足 ReadWriterCloser

10. 自测题

10.1 概念题

  1. Go 为什么没有继承?
  2. 嵌入和继承的核心区别是什么?
  3. 嵌入类型的方法怎么被外层类型调用?
  4. 一个类型可以嵌入多个其他类型吗?
  5. "接收接口,返回结构体"是什么意思?
  6. 接口可以嵌入其他接口吗?
  7. 嵌入和命名字段有什么区别?
  8. 什么时候用命名字段,什么时候用嵌入?

11. 本课总结

这一课你学到了 Go 的核心设计哲学——组合优于继承。

你现在应该已经理解:

  • Go 用组合(嵌入)和接口代替继承
  • 嵌入让外层类型获得内层类型的字段和方法
  • 接口定义行为,组合共享实现
  • 接口 + 组合可以实现灵活、解耦的设计
  • 不要过度嵌入,保持类型职责清晰
  • 嵌入不等于继承——Dog 嵌入 Animal 不意味着 Dog is Animal

12. 下一课预告

下一课我们学习 Go 中非常重要的主题:错误处理 error

会重点讲:

  • error 接口是什么
  • 怎么创建和返回错误
  • errors.Newfmt.Errorf 的区别
  • 错误处理的最佳实践

学完下一课,你就能写出清晰可靠的错误处理逻辑了。