Go 嵌入与组合
Go 通过结构体嵌入和接口嵌入实现组合复用,这是 Go 面向对象设计的核心——组合优于继承。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 类型系统与抽象 MOC
- 前置概念: [[go-structs|Go 结构体]], Go 接口, [[go-methods|Go 方法]]
- 并列概念: Go struct 标签
Go 嵌入与组合
一句话定义
Go 通过在结构体中嵌入类型(匿名字段)或在接口中嵌入其他接口,实现方法和行为的组合复用,取代传统面向对象语言的继承机制。
核心机制 / 工作原理
结构体嵌入(Struct Embedding)
将一个类型(结构体或基本类型的命名类型)作为匿名字段放入另一个结构体。嵌入后,外层类型自动获得内层类型的所有方法和字段——这就是方法提升(method promotion)。
type Logger struct{}
func (l Logger) Log(msg string) { fmt.Println(msg) }
type Server struct {
Logger // 嵌入,而非 Logger Logger
Addr string
}
s := Server{Addr: ":8080"}
s.Log("started") // 直接调用,编译器自动提升到 s.Logger.Log("started")
接口嵌入(Interface Embedding)
接口可以嵌入其他接口,效果是取并集:
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 没有类继承(没有 extends、subclass),设计哲学是:
- 显式优于隐式:嵌入关系在结构体定义中一目了然,不存在深层继承链。
- 扁平优于层次:一个结构体可以嵌入多个类型,形成宽而扁的组合,而非深而窄的继承树。
- 接口解耦:通过小接口的嵌入组合出大接口,依赖者只需关心自己需要的行为。
菱形问题(Diamond Problem)
在有继承的语言中,如果 B 和 C 都继承 A,D 同时继承 B 和 C,D 中 A 的方法只有一份——这就是菱形问题。Go 中不存在此问题:嵌入是组合而非继承,每层嵌入都有自己的字段空间,同名字段/方法需要显式消歧。
命名冲突与消歧
当多个嵌入类型提供了同名方法或字段时,外层类型不再自动提升,必须显式指定路径:
type A struct{}
func (A) Hello() { fmt.Println("A") }
type B struct{}
func (B) Hello() { fmt.Println("B") }
type C struct {
A
B
}
// c.Hello() // 编译错误: ambiguous selector c.Hello
c.A.Hello() // 正确: 显式指定
最小例子 / 最小场景
package main
import (
"fmt"
"sync"
)
// 嵌入 sync.Mutex 获得锁能力
type SafeMap struct {
sync.Mutex
data map[string]int
}
func (m *SafeMap) Set(key string, value int) {
m.Lock()
defer m.Unlock()
m.data[key] = value
}
func (m *SafeMap) Get(key string) (int, bool) {
m.Lock()
defer m.Unlock()
v, ok := m.data[key]
return v, ok
}
func main() {
sm := SafeMap{data: make(map[string]int)}
sm.Set("count", 42) // 直接用 sm 的方法
sm.Lock() // 也可以直接用嵌入的 Mutex 方法
sm.data["extra"] = 1
sm.Unlock()
v, _ := sm.Get("count")
fmt.Println(v) // 42
}
为什么重要
- 代码复用的惯用方式:Go 标准库和主流框架大量使用嵌入组合。
sync.Mutex嵌入自定义结构体、http.Handler嵌入中间件链、ORM 模型嵌入基础字段——这都是日常编码。 - 避免继承的脆弱性:继承链的任何一层改动都可能影响所有子类。组合关系更松散,内层类型的变化对组合者影响可控。
- 接口的渐进式扩展:
io.Reader、io.Writer、io.Closer等小接口通过嵌入组合出io.ReadWriteCloser,组合者只实现自己需要的部分。 - 可测试性:嵌入的组件可以被 mock 或替换(通过接口约束),测试更灵活。
边界与易混淆点
- 嵌入不是继承:
Server嵌入Logger后,Server不是Logger的子类。Server类型的值不能赋值给Logger类型的变量,除非Server实现了Logger实现的接口。 - 嵌入指针 vs 嵌入值:嵌入
*Mutexvs 嵌入Mutex有区别。嵌入指针时,外层结构体的零值不可直接使用(指针为 nil),需要先初始化。 - 同名方法冲突需显式消歧:编译器不会自动选择”更具体”的实现,只要有两个候选且路径不同,就报 ambiguous 错误。
- 嵌入类型的字段在外层不可直接按名访问(如果存在歧义):虽然一般可以直接访问嵌入类型的字段(如
s.Addr),但多层嵌入同名字段时需要完整路径。 - 未导出类型的嵌入有可见性限制:嵌入未导出类型后,其方法在外包中不可用,虽然在包内可以正常提升。