Go 嵌入与组合

Go 通过结构体嵌入和接口嵌入实现组合复用,这是 Go 面向对象设计的核心——组合优于继承。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

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 没有类继承(没有 extendssubclass),设计哲学是:

  • 显式优于隐式:嵌入关系在结构体定义中一目了然,不存在深层继承链。
  • 扁平优于层次:一个结构体可以嵌入多个类型,形成宽而扁的组合,而非深而窄的继承树。
  • 接口解耦:通过小接口的嵌入组合出大接口,依赖者只需关心自己需要的行为。

菱形问题(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.Readerio.Writerio.Closer 等小接口通过嵌入组合出 io.ReadWriteCloser,组合者只实现自己需要的部分。
  • 可测试性:嵌入的组件可以被 mock 或替换(通过接口约束),测试更灵活。

边界与易混淆点

  • 嵌入不是继承Server 嵌入 Logger 后,Server 不是 Logger 的子类。Server 类型的值不能赋值给 Logger 类型的变量,除非 Server 实现了 Logger 实现的接口。
  • 嵌入指针 vs 嵌入值:嵌入 *Mutex vs 嵌入 Mutex 有区别。嵌入指针时,外层结构体的零值不可直接使用(指针为 nil),需要先初始化。
  • 同名方法冲突需显式消歧:编译器不会自动选择”更具体”的实现,只要有两个候选且路径不同,就报 ambiguous 错误。
  • 嵌入类型的字段在外层不可直接按名访问(如果存在歧义):虽然一般可以直接访问嵌入类型的字段(如 s.Addr),但多层嵌入同名字段时需要完整路径。
  • 未导出类型的嵌入有可见性限制:嵌入未导出类型后,其方法在外包中不可用,虽然在包内可以正常提升。
创建于 2026/6/25 更新于 2026/6/25