一、基本语法区别
组合(Composition)—— 命名字段
package main import "fmt" type Engine struct { Power int } type Car struct { myEngine Engine // 有名字 myEngine,这是普通组合 Brand string } func main() { c := Car{ myEngine: Engine{Power: 100}, Brand: "Toyota", } // 必须通过字段名访问 fmt.Println(c.myEngine.Power) }嵌入(Embedding)—— 匿名字段
package main import "fmt" type Engine struct { Power int } type Car struct { Engine // 没有字段名,只有类型名,这是嵌入 Brand string } func main() { c := Car{ Engine: Engine{Power: 100}, Brand: "Toyota", } // 可以直接访问嵌入类型的字段 fmt.Println(c.Power) // 等价于 c.Engine.Power fmt.Println(c.Engine.Power) // 也可以这样写 }二、方法提升(Method Promotion)区别
这是两者最关键的区别之一。
组合:方法不会提升
package main import "fmt" type Engine struct{} func (e Engine) Start() { fmt.Println("Engine starting...") } type Car struct { eng Engine // 命名字段:普通组合 } func main() { c := Car{eng: Engine{}} // c.Start() // ❌ 编译错误!Car 没有 Start 方法 c.eng.Start() // ✅ 只能通过字段名调用 }嵌入:方法自动提升
package main import "fmt" type Engine struct{} func (e Engine) Start() { fmt.Println("Engine starting...") } type Car struct { Engine // 匿名字段:嵌入 } func main() { c := Car{Engine: Engine{}} c.Start() // ✅ 直接调用!等价于 c.Engine.Start() c.Engine.Start() // ✅ 也可以这样写 }方法提升的本质:嵌入后,外层结构体仿佛"拥有"了内层结构体的所有方法。
三、接口实现区别(最实用的差异)
这是实际开发中最容易踩坑、也最能体现嵌入威力的地方。
组合:外层不会自动实现内层的接口
package main import "fmt" type Starter interface { Start() } type Engine struct{} func (e Engine) Start() { fmt.Println("Engine started") } // 普通组合 type Car struct { myEngine Engine // 命名字段 } func main() { var s Starter // s = Car{} // ❌ 编译错误!Car 没有实现 Start() 方法 // 必须自己再写一遍 s = Engine{} // ✅ 只能赋值 Engine 本身 s.Start() }嵌入:外层自动实现内层的接口
package main import "fmt" type Starter interface { Start() } type Engine struct{} func (e Engine) Start() { fmt.Println("Engine started") } // 嵌入 type Car struct { Engine // 匿名字段 } func main() { var s Starter s = Car{} // ✅ 编译通过!Car 自动实现了 Starter 接口 s.Start() // 输出:Engine started }关键点:嵌入时,Go 编译器会自动把内层类型的方法"提升"到外层,使得外层类型也满足这些方法对应的接口。
四、方法覆盖(同名方法处理)
嵌入时,外层可以覆盖内层方法
package main import "fmt" type Engine struct{} func (e Engine) Start() { fmt.Println("Engine start") } type Car struct { Engine } // Car 自己实现了 Start(),会覆盖 Engine 的 Start() func (c Car) Start() { fmt.Println("Car start with key") } func main() { c := Car{} c.Start() // 输出:Car start with key(调用 Car 自己的) c.Engine.Start() // 输出:Engine start(调用嵌入的 Engine 的) }组合时不存在"覆盖"概念
因为组合没有方法提升,所以外层和内层的方法完全是独立的,不存在覆盖问题。
五、多重嵌入与冲突
嵌入支持多重嵌入,但如果两个嵌入类型有同名字段或方法,会产生冲突。
package main type A struct { Name string } type B struct { Name string // 和 A 同名字段 } type C struct { A B // 嵌入 A 和 B } func main() { c := C{} // c.Name = "hello" // ❌ 编译错误!Name 不明确,不知道用 A.Name 还是 B.Name c.A.Name = "hello" // ✅ 必须显式指定 c.B.Name = "world" // ✅ }六、完整对比表格
| 对比维度 | 组合(Composition) 命名字段 | 嵌入(Embedding) 匿名字段 |
|---|---|---|
| 语法 | fieldName Type | Type(只有类型名,没有字段名) |
| 关系语义 | "has-a"(有一个) | "has-a"(有一个,但更紧密) |
| 字段访问 | 必须通过字段名:c.fieldName.Field | 可直接访问:c.Field(也可c.Type.Field) |
| 方法提升 | ❌ 不会提升,外层不能直接调用内层方法 | ✅ 自动提升,外层可直接调用内层方法 |
| 接口实现 | ❌ 外层不会自动实现内层已实现的接口 | ✅ 外层自动实现内层已实现的所有接口 |
| 方法覆盖 | 不存在覆盖概念(方法独立) | ✅ 外层可实现同名方法覆盖内层 |
| 多重组合/嵌入 | 无冲突问题 | 同名字段/方法会产生歧义,需显式指定 |
| 初始化方式 | FieldName: Value | TypeName: Value |
| JSON 序列化 | 字段名作为 JSON key | 嵌入类型的字段会"展开"到外层(除非加标签) |
| 使用场景 | 松耦合、需要明确区分层次关系 | 代码复用、快速实现接口(如装饰器模式) |
七、一句话总结
组合是"把一个结构体放进另一个结构体里当类型用",嵌入是"把一个结构体融进另一个结构体里,让它共享自己的字段和方法"。
嵌入是 Go 语言实现"组合优于继承"的核心语法机制,它用类似"继承"的语法(方法提升、接口自动实现)实现了组合的灵活性,同时避免了继承的耦合问题。