Go 的设计哲学:Composition over Inheritance(组合优于继承)。没有 extends、没有 protected、没有多态层级——Go 用更少的语言特性达到了同样的目标。但思维方式需要转弯。


Q1:Go 真的不能”继承”吗?那代码怎么复用?

A: Go 用 组合(Composition)+ 嵌入(Embedding) 替代继承。先看三种组合模式:

模式一:匿名嵌入(最接近”继承”的感觉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Engine struct {
Power int // 马力
}

func (e *Engine) Start() {
fmt.Printf("引擎启动!%d 马力\n", e.Power)
}

type Car struct {
Engine // 匿名嵌入 —— Car 自动拥有 Engine 的字段和方法
Brand string
}

c := &Car{Engine: Engine{Power: 300}, Brand: "Tesla"}
c.Start() // "引擎启动!300 马力"
fmt.Println(c.Power) // 300,直接访问嵌入的字段

优点:写法简洁,自动获得被嵌入类型的方法。
缺点:容易误以为这是真正的继承。

模式二:显式字段(推荐)

1
2
3
4
5
6
7
8
type Car struct {
engine *Engine // 显式声明,语义清晰
Brand string
}

func (c *Car) Start() {
c.engine.Start() // 显式调用,一目了然
}

为什么推荐? 调用处能清楚看到方法来自哪里,不会出现命名冲突的意外。

模式三:接口组合(解耦复用)

1
2
3
4
5
6
7
8
type Starter interface { Start() }
type Mover interface { Move() }

// 通过接口约束行为,不关心具体实现
type Vehicle interface {
Starter
Mover
}

Q2:匿名嵌入的方法冲突怎么办?

A: 当两个嵌入类型有同名方法时:

1
2
3
4
5
6
7
8
9
10
11
12
type A struct{}
func (a *A) Foo() { fmt.Println("A.Foo") }

type B struct{}
func (b *B) Foo() { fmt.Println("B.Foo") }

type C struct {
A // 都有 Foo()
B // 都有 Foo()
}

// c.Foo() ❌ 编译错误!ambiguous selector c.Foo

解决办法——显式选择:

1
2
3
4
5
6
7
c := &C{}
c.A.Foo() // "A.Foo"
c.B.Foo() // "B.Foo"

// 如果 C 自己定义了 Foo,则覆盖
func (c *C) Foo() { fmt.Println("C.Foo") }
c.Foo() // "C.Foo" ✅

这就是为什么显式字段比匿名嵌入更安全——冲突在编译时就暴露了。

Q3:Go 怎么实现类似”抽象类”的效果?

A: 用接口 + 默认实现的组合:

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
// 定义行为接口
type GameEntity interface {
Update(dt float64)
Render()
}

// 提供默认实现(基类的感觉)
type BaseEntity struct {
Position Vector2
Health int
}

func (b *BaseEntity) TakeDamage(amount int) {
b.Health -= amount
if b.Health <= 0 {
b.OnDeath()
}
}

func (b *BaseEntity) OnDeath() {
fmt.Println("实体死亡")
}

// 具体实体"继承"基类并扩展
type Enemy struct {
BaseEntity // 嵌入基类
EnemyType string
AggroRange float32
}

// 可以覆盖基类方法
func (e *Enemy) OnDeath() {
fmt.Printf("%s 死亡!掉落奖励\n", e.EnemyType)
// 还可以调父类逻辑吗?
// 不能直接调用 BaseEntity.OnDeath(e),但可以:
}

// 实现接口要求的其他方法
func (e *Enemy) Update(dt float64) {
// AI 逻辑...
}

func (e *Enemy) Render() {
// 渲染逻辑...
}

这其实就是你在 godot-game-one 项目里用 GDScript 写的那种模式——只是 Go 更严格。

Q4:和 Java/C# 的继承对比,Go 到底亏了还是赚了?

A: 直接说结论——大多数场景是赚了,少数场景确实不方便。

能力 Java 继承 Go 组合 评价
代码复用 extends 一行搞定 嵌入或手动委托 Go 多写几行,但不复杂
方法覆盖 @Override 清晰明确 同名方法直接覆盖 平手
调用父类方法 super.foo() 不支持! 这是真痛点 Go 亏
多态 父类引用→子类对象 接口满足即可 Go 更灵活
钻石问题 有(interface default) 不存在 Go 赢
层级过深 容易写出 7 层继承链 天然限制 Go 大赢

“不能调父类方法”怎么办?

实际项目中的 workaround:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方案一:包装方法
func (e *Enemy) OnDeath() {
// 手动调用基类逻辑
fmt.Println("敌人特殊死亡效果...")
e.BaseEntity.OnDeath() // 嵌入后可以直接调!
}

// 方案二:钩子函数模式
func (b *BaseEntity) OnDeath() {
b.beforeDeath() // 子类可覆盖的钩子
fmt.Println("通用死亡逻辑")
b.afterDeath() // 子类可覆盖的钩子
}

func (e *Enemy) beforeDeath() {
fmt.Println("播放死亡动画")
}

方案二其实是你游戏开发中常用的——Godot 的 _ready() / _process() 就是这种钩子思维。

Q5:什么时候用嵌入,什么时候用接口?

A: 决策树:

1
2
3
4
5
需要复用代码(字段+方法)?
├── 是 → 用结构体嵌入(匿名 or 显式)
│ └── 需要多态/解耦?→ 再套一层接口
└── 否 → 只需要定义行为契约?
└── 用接口就够了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 场景1:HTTP 中间件 —— 用接口
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}

// 场景2:玩家角色有基础属性 —— 用嵌入
type Player struct {
Character // 嵌入基础角色
Inventory // 嵌入背包系统
UserID string
}

// 场景3:日志组件 —— 两者结合
type Logger struct {
writer io.Writer // 接口字段(灵活替换输出目标)
prefix string
}

Q6:Go 有设计模式吗?常用的有哪些?

A: 有,而且因为语言简洁,很多模式比在其他语言中更干净:

Strategy(策略模式)—— 用接口天然支持

1
2
3
4
5
6
7
8
9
10
11
12
13
type PaymentStrategy interface {
Pay(amount float64) error
}

type Alipay struct{} // 支付宝
type WeChatPay struct{} // 微信支付

func (a *Alipay) Pay(amt float64) error { /* ... */ }
func (w *WeChatPay) Pay(amt float64) error { /* ... */ }

func Checkout(s PaymentStrategy, amt float64) error {
return s.Pay(amt)
}

Decorator(装饰器模式)—— 用嵌入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type DataSource interface { Read() string }

// 基础实现
type FileSource struct{ Path string }

// 装饰器:缓存
type CachedSource struct {
DataSource // 嵌入
cache string
}

// 装饰器:日志
type LoggingSource struct {
DataSource
logger *log.Logger
}

Option(选项模式)—— 上篇聊过的工厂模式变体

1
2
3
4
5
6
7
8
9
type ServerOption func(*Server)

func WithPort(port int) ServerOption {
return func(s *Server) { s.port = port }
}

func WithTimeout(d time.Duration) ServerOption {
return func(s *Server) { s.timeout = d }
}

Q7:面向对象三篇总结一下,Go OOP 的核心心智模型是什么?

A:

传统 OOP 概念 Go 对应物 核心区别
class struct + methods 数据和行为分离定义
extends embedding has-a 不是 is-a
implements implicit satisfaction 无需声明
this/self receiver 显式接收者
protected/private exported/unexported 包级别可见性
abstract class interface + concrete type 分离得更彻底
super.method() 不支持 用钩子/包装绕行

一句话总结:

Go 的 OOP 是”小而精确”的——它砍掉了继承层级、砍掉了构造函数、砍掉了隐式 this。代价是有些地方要手动多写一点,换来的是代码关系永远清晰可见。你永远不会在 Go 里遇到”这个方法到底在哪定义的?”这种问题。


本系列回顾与预告

  • go-01: 十问十答 — 变量、类型、流程控制、并发入门
  • go-02: 结构体与方法 — 工厂模式、接收者选择、标签、内存对齐
  • go-03: 接口与鸭子类型 — 隐式接口、nil 接口陷阱、error 本质
  • go-04: 组合优于继承 — 三种组合模式、设计模式、OOP 总结

接下来进入 工程实践篇:Error 处理最佳实践 → Context 包 → Testing。这些才是真正写生产代码时每天打交道的东西。

TODO: 拿你 slg-go 或 game-bass 项目里的一个核心 struct 审视一下——有没有哪里用了隐式嵌入导致调用来源不清晰的?有没有该用接口却写了具体类型的?改完之后你会发现代码的可测试性提升了一个档次。