8.3 Go Struct 嵌入学习笔记

8.3 Go Struct 嵌入学习笔记

1. 结构体嵌入(匿名嵌入)

Go 没有类的继承,但可以通过匿名嵌入一个结构体来获得组合能力:

type Engine struct {
    Horsepower int
    Type       string
}

func (e Engine) Start() {
    fmt.Printf("Starting %s engine with %d HP\n", e.Type, e.Horsepower)
}

func (e Engine) Stop() {
    fmt.Printf("Stopping %s engine\n", e.Type)
}

type Car struct {
    Make   string
    Model  string
    Year   int
    Engine  // 匿名嵌入 Engine,没有字段名
}

匿名嵌入 vs 具名嵌入

结构体嵌入有两种写法:

匿名嵌入(嵌入式) — 不写字段名,方法提升:

type Car struct {
    Make   string
    Engine  // 不写字段名
}
// 访问:car.Start()、car.Horsepower(字段和方法自动提升)

具名嵌入(普通字段) — 写字段名,没有提升:

type Car struct {
    Make   string
    Motor  Engine  // 写字段名
}
// 访问:car.Motor.Start()、car.Motor.Horsepower(必须通过字段名)

区别:

匿名嵌入 具名嵌入
写法 Engine Motor Engine
字段访问 car.Horsepower(提升) car.Motor.Horsepower
方法访问 car.Start()(提升) car.Motor.Start()
接口实现 外层自动满足嵌入类型的接口 外层不自动满足

匿名嵌入的核心优势是方法提升——嵌入类型的字段和方法可以直接在外层使用。具名嵌入则没有任何提升效果,只是普通的结构体字段。

嵌入后 Car 可以直接访问 Engine 的字段和方法:

car := Car{
    Make:  "Toyota",
    Model: "Camry",
    Year:  2023,
    Engine: Engine{Horsepower: 200, Type: "V6"},
}

car.Stop()          // 直接调用 Engine 的 Stop 方法(字段提升)
car.Engine.Start()  // 也可以显式通过 Engine 访问

执行结果:

Stopping V6 engine
Starting V6 engine with 200 HP

要点:匿名嵌入不需要字段名,Go 自动用类型名作为字段名(Engine),嵌入的字段和方法会"提升"到外层结构体。


2. 方法提升与覆盖

当外层结构体和嵌入结构体有同名方法时,外层的方法会覆盖嵌入的方法:

// Engine 的 Start 方法
func (e Engine) Start() {
    fmt.Printf("Starting %s engine with %d HP\n", e.Type, e.Horsepower)
}

// Car 的 Start 方法 — 覆盖了 Engine 的 Start
func (c Car) Start() {
    fmt.Printf("Starting %d %s %s\n", c.Year, c.Make, c.Model)
    c.Engine.Start()  // 在覆盖方法中显式调用嵌入的方法
}

调用结果:

car.Start()         // 调用 Car 的 Start(覆盖版本)
car.Engine.Start()  // 显式调用 Engine 的 Start(原始版本)

执行结果:

Starting 2023 Toyota Camry    ← Car.Start() 覆盖方法
Starting V6 engine with 200 HP ← Car.Start() 中显式调用了 Engine.Start()
Starting V6 engine with 200 HP ← car.Engine.Start() 直接调用

要点

  • car.Start() → 调用 Car 自己的 Start(覆盖)
  • car.Engine.Start() → 调用 Engine 的 Start(原始)
  • 覆盖方法中可以用 c.Engine.Start() 调用被覆盖的方法,类似其他语言的 super

3. 多层嵌入

结构体可以嵌入多层,方法会逐层提升:

type LuxuryCar struct {
    Car          // 嵌入 Car(Car 里又嵌入了 Engine)
    GPS          // 嵌入 GPS
    Radio        // 嵌入 Radio
    LeatherSeats bool
}

LuxuryCar 可以直接调用所有嵌入层的方法:

luxuryCar := LuxuryCar{
    Car: Car{
        Make:  "BMW", Model: "X7", Year: 2024,
        Engine: Engine{Horsepower: 400, Type: "V8"},
    },
    GPS:   GPS{Brand: "Garmin", HasMaps: true},
    Radio: Radio{Brand: "Bose", HasBluetooth: true},
    LeatherSeats: true,
}

luxuryCar.Start()            // Car 的 Start(覆盖了 Engine 的 Start)
luxuryCar.Drive()            // Car 的 Drive
luxuryCar.Navigate("Downtown") // GPS 的 Navigate
luxuryCar.PlayMusic()         // Radio 的 PlayMusic

执行结果:

Starting 2024 BMW X7
Starting V8 engine with 400 HP
Driving 2024 BMW X7
Garmin GPS navigating to Downtown
Playing music on Bose radio

要点:嵌入链 Car → Engine → Start/Stop,LuxuryCar → Car → Drive/Start,LuxuryCar 可以直接调用所有层级提升的方法。


4. 字段名冲突

当嵌入的多个结构体有同名字段时,直接访问会编译报错,必须显式指定:

type Person struct {
    Name string
    Age  int
}

type Company struct {
    Name      string
    Employees int
}

type Employee struct {
    ID       int
    Position string
    Person   // 嵌入 Person(有 Name 字段)
    Company  // 嵌入 Company(也有 Name 字段)
}

PersonCompany 都有 Name 字段,产生冲突:

// emp.Name ← 编译错误!ambiguous field Name
// 必须显式指定:
emp.Person.Name   // "John Doe"
emp.Company.Name   // "Tech Corp"

完整访问示例:

emp := Employee{
    ID: 1001, Position: "Software Engineer",
    Person:  Person{Name: "John Doe", Age: 30},
    Company: Company{Name: "Tech Corp", Employees: 500},
}

fmt.Printf("Employee: %s (ID: %d)\n", emp.Person.Name, emp.ID)
fmt.Printf("Works at: %s\n", emp.Company.Name)
fmt.Printf("Age: %d\n", emp.Person.Age)

执行结果:

Employee: John Doe (ID: 1001)
Works at: Tech Corp
Position: Software Engineer
Age: 30

要点:字段冲突时不能直接用 emp.Name,必须用 emp.Person.Nameemp.Company.Name 显式指定来源。


5. 嵌入与接口

嵌入的结构体方法也会被提升到外层,使得外层结构体自动实现嵌入结构体所满足的接口:

// Car 嵌入了 Engine,所以 Car 也拥有 Engine 的所有方法
// 如果某个接口要求 Start() 和 Stop(),Engine 满足的话 Car 也满足

var engine Engine = car.Engine  // 提取嵌入的结构体
engine.Start()

执行结果:

Starting V6 engine with 200 HP

要点:嵌入不是继承,但可以实现类似"接口继承"的效果——嵌入结构体满足的接口,外层结构体也自动满足。


6. 嵌入 vs 继承

Go 的嵌入与传统继承的关键区别:

继承(Java/C++) 嵌入(Go)
关系 子类是父类的一种(is-a) 组合关系(has-a),但方法可提升
类型 子类可以赋值给父类变量 外层结构体不能赋值给嵌入类型变量
方法覆盖 子类覆盖父类方法 外层方法覆盖嵌入方法,但可以显式调用嵌入版本
多态 通过继承实现 通过接口实现
// ❌ 不可以这样赋值
// var eng Engine = car  // 编译错误!Car 不是 Engine

// ✅ 只能提取嵌入的结构体
var eng Engine = car.Engine  // 正确

知识点总结

知识点 关键概念
匿名嵌入 不写字段名,Go 用类型名作为字段名,字段和方法自动提升
方法提升 嵌入结构体的方法可以直接在外层调用
方法覆盖 外层同名方法覆盖嵌入方法,可用 c.EmbedType.Method() 显式调用
多层嵌入 嵌入链逐层提升,LuxuryCar 可调用 Engine、Car、GPS、Radio 的方法
字段冲突 多个嵌入有同名字段时,必须显式指定来源 emp.Person.Name
接口提升 嵌入结构体满足的接口,外层也自动满足
嵌入≠继承 Car 不能赋值给 Engine 变量,组合而非 is-a 关系
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容