8.3 Go Struct 嵌入学习笔记

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 字段)
}

PersonCompany 都有 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.Nameemp.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 关系
相关推荐
xiangjiaodalishi19 分钟前
2026年GEO生成式引擎优化服务商观察:企业如何挑选适配自身的AI营销协作伙伴
go
蛋先生DX1 小时前
明明都是源码到CPU,各语言中间原理咋不是一个套路?
java·javascript·go
西瓜太郎4991 天前
从请求到上游:实现一条可观测的多模型调用链
go
newerp1 天前
Golang 切片底层结构
后端·程序员·go
运维开发笔记2 天前
8.2 Go Struct 方法学习笔记
go
Go_error2 天前
Fyne:让 Go 开发者也能玩转 GUI
后端·go
Go_error2 天前
Go 实现 Mysql AES 与 Scanner/Valuer 自动加解密
go
Go_error2 天前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error2 天前
Go-redis:执行 Lua 脚本
后端·go