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 Brand string }
c := &Car{Engine: Engine{Power: 300}, Brand: "Tesla"} c.Start() fmt.Println(c.Power)
|
优点:写法简洁,自动获得被嵌入类型的方法。
缺点:容易误以为这是真正的继承。
模式二:显式字段(推荐)
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 B }
|
解决办法——显式选择:
1 2 3 4 5 6 7
| c := &C{} c.A.Foo() c.B.Foo()
func (c *C) Foo() { fmt.Println("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) }
func (e *Enemy) Update(dt float64) { }
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
| type Handler interface { ServeHTTP(w http.ResponseWriter, r *http.Request) }
type Player struct { Character Inventory UserID string }
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 审视一下——有没有哪里用了隐式嵌入导致调用来源不清晰的?有没有该用接口却写了具体类型的?改完之后你会发现代码的可测试性提升了一个档次。