go语言的组合和嵌入傻傻分不清?

一、基本语法区别

组合(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 语言实现"组合优于继承"的核心语法机制,它用类似"继承"的语法(方法提升、接口自动实现)实现了组合的灵活性,同时避免了继承的耦合问题。