8.3 Go Struct 嵌入学习笔记
1. 结构体嵌入(匿名嵌入)
Go 没有类的继承,但可以通过匿名嵌入一个结构体来获得组合能力:
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 具名嵌入
结构体嵌入有两种写法:
匿名嵌入(嵌入式) --- 不写字段名,方法提升:
go
type Car struct {
Make string
Engine // 不写字段名
}
// 访问:car.Start()、car.Horsepower(字段和方法自动提升)
具名嵌入(普通字段) --- 写字段名,没有提升:
go
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 的字段和方法:
go
car := Car{
Make: "Toyota",
Model: "Camry",
Year: 2023,
Engine: Engine{Horsepower: 200, Type: "V6"},
}
car.Stop() // 直接调用 Engine 的 Stop 方法(字段提升)
car.Engine.Start() // 也可以显式通过 Engine 访问
执行结果:
csharp
Stopping V6 engine
Starting V6 engine with 200 HP
要点 :匿名嵌入不需要字段名,Go 自动用类型名作为字段名(Engine),嵌入的字段和方法会"提升"到外层结构体。
2. 方法提升与覆盖
当外层结构体和嵌入结构体有同名方法时,外层的方法会覆盖嵌入的方法:
go
// 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() // 在覆盖方法中显式调用嵌入的方法
}
调用结果:
go
car.Start() // 调用 Car 的 Start(覆盖版本)
car.Engine.Start() // 显式调用 Engine 的 Start(原始版本)
执行结果:
scss
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. 多层嵌入
结构体可以嵌入多层,方法会逐层提升:
go
type LuxuryCar struct {
Car // 嵌入 Car(Car 里又嵌入了 Engine)
GPS // 嵌入 GPS
Radio // 嵌入 Radio
LeatherSeats bool
}
LuxuryCar 可以直接调用所有嵌入层的方法:
go
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
执行结果:
vbnet
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. 字段名冲突
当嵌入的多个结构体有同名字段时,直接访问会编译报错,必须显式指定:
go
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 字段)
}
Person 和 Company 都有 Name 字段,产生冲突:
go
// emp.Name ← 编译错误!ambiguous field Name
// 必须显式指定:
emp.Person.Name // "John Doe"
emp.Company.Name // "Tech Corp"
完整访问示例:
go
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)
执行结果:
yaml
Employee: John Doe (ID: 1001)
Works at: Tech Corp
Position: Software Engineer
Age: 30
要点 :字段冲突时不能直接用 emp.Name,必须用 emp.Person.Name 或 emp.Company.Name 显式指定来源。
5. 嵌入与接口
嵌入的结构体方法也会被提升到外层,使得外层结构体自动实现嵌入结构体所满足的接口:
go
// Car 嵌入了 Engine,所以 Car 也拥有 Engine 的所有方法
// 如果某个接口要求 Start() 和 Stop(),Engine 满足的话 Car 也满足
var engine Engine = car.Engine // 提取嵌入的结构体
engine.Start()
执行结果:
csharp
Starting V6 engine with 200 HP
要点:嵌入不是继承,但可以实现类似"接口继承"的效果------嵌入结构体满足的接口,外层结构体也自动满足。
6. 嵌入 vs 继承
Go 的嵌入与传统继承的关键区别:
| 继承(Java/C++) | 嵌入(Go) | |
|---|---|---|
| 关系 | 子类是父类的一种(is-a) | 组合关系(has-a),但方法可提升 |
| 类型 | 子类可以赋值给父类变量 | 外层结构体不能赋值给嵌入类型变量 |
| 方法覆盖 | 子类覆盖父类方法 | 外层方法覆盖嵌入方法,但可以显式调用嵌入版本 |
| 多态 | 通过继承实现 | 通过接口实现 |
go
// ❌ 不可以这样赋值
// 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 关系 |