Go从入门到精通——结构体(struct)——类型内嵌和结构体内嵌
类型内嵌和结构体内嵌
结构体允许其他成员字段在声明时没有字段名而只有类型,这种形式的字段被称为类型内嵌或匿名字段。
类型内嵌的写法如下:
1 2 3 4 5 6 7 8 9 10 11 | type Data struct { int float32 bool } ins := &Data{ int: 10, float32: 3.14, bool: true, } |
类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而已,结构体要求字段名必须唯一,因此一个结构体中同样类型的匿名字段只能有一个。
结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。
一、声明结构体内嵌
结构体类型内嵌比普通类型内嵌的概念复杂一些,下面通过一个实例来理解。
计算机图形学中的颜色有两种类型,一种是包含 红、绿、蓝的基础颜色;另外一种是在基础颜色之外增加透明度的颜色。透明度在颜色中叫 Alpha,范围为 0-10 之间。0 表示完全透明,1 表示不透明。使用传统的结构体字段的方法定义基础颜色和带有透明度颜色的过程代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package main //基础颜色 type BasicColor struct { //红 绿 蓝 R, G, B float32 } //完整的颜色定义 type Color struct { //将基本颜色作为成员 Basic BasicColor //透明度 Alpha float32 } func main() { var c Color //设置基本颜色分量 c.Basic.R = 1 c.Basic.B = 1 c.Basic.G = 0 } |
通过 Basic 结构才能设置 R、G、B 分量,虽然合理但是写法很复杂。使用 Go 语言的结构体内嵌写法重新调整代码如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package main type BasicColor struct { R, G, B float32 } type Color struct { BasicColor //将BasicColor 结构体嵌入到 Color 结构体中,BasicColor 没有字段名而只有类型,这种写法叫结构体内嵌。 Alpha float32 } func main() { var c Color c.R = 1 c.G = 1 c.B = 0 } |
二、结构内嵌特性
Go 语言的结构体内嵌有如下特性
1、内嵌的结构体可以直接访问其成员变量
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c 的访问可以简化 ins.c。
2、内嵌结构体的字段名是它的类型
内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名,代码如下:
1 2 3 4 | var c Color c.Basic.R = 1 c.Basic.B = 1 c.Basic.G = 0 |
一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况。
三、使用组合思想描述对象特性
在面向对象思想中,实现对象关系需要使用 “继承” 特性。例如,人类不能飞行,鸟类可以飞行。人类和鸟类都可以继承自可行走类,但只有鸟类继承飞行类。
面向对象的设计原则中也建议对象最好不要使用多重继承 ,有些面向对象语言从语言层面就禁止了多重继承,如 C# 和 Java 语言。鸟类同时继承自可行走类和飞行类,这显然是存在问题的。
在面向对象思想中,要正确地实现对象的多重特性,只能使用一些精巧的设计来补救。
Go 语言的结构体内嵌特性是一种组合特性,使用组合特性可以快速构建对象的不同特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package main import ( "fmt" ) type Flying struct {} type Walking struct {} func (f *Flying) Fly() { fmt.Println( "我能飞" ) } func (w *Walking) Walk() { fmt.Println( "我能走" ) } type Bird struct { Walking Flying } type Human struct { Walking } func main() { //实例化鸟类 b := new(Bird) fmt.Println( "鸟:" ) b.Fly() b.Walk() //实例化人类 h := new(Human) fmt.Println( "人: " ) h.Walk() } |
运行结果输出如下:
1 2 3 4 5 6 7 8 9 10 | Starting: D:\ go -testfiles\bin\dlv.exe dap --check- go -version=false --listen=127.0.0.1:57732 from d:\ go -testfiles DAP server listening at: 127.0.0.1:57732 Type 'dlv help' for list of commands. 鸟: 我能飞 我能走 人: 我能走 Process 17532 has exited with status 0 Detaching |
四、初始化结构体内嵌
结构体内嵌初始化时,将结构体内嵌的类型作为字段名像普通结构体一样进行初始化,详细实现过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package main import ( "fmt" ) //车轮 type wheel struct { Size int } //引擎 type Engine struct { Powrer int Type string } //车 type Car struct { wheel Engine } func main() { c := Car{ //初始化轮子 wheel: wheel{ Size: 18, }, //初始化引擎 Engine: Engine{ Powrer: 200, Type: "2.0T" , }, } fmt.Printf( "%+v\n" , c) } |
运行结果输出如下:
1 2 3 4 5 6 7 | Starting: D:\ go -testfiles\bin\dlv.exe dap --check- go -version=false --listen=127.0.0.1:58140 from d:\ go -testfiles DAP server listening at: 127.0.0.1:58140 Type 'dlv help' for list of commands. {wheel:{Size:18} Engine:{Powrer:200 Type:2.0T}} Process 15304 has exited with status 0 Detaching dlv dap (9788) exited with code: 0 |
五、初始化内嵌匿名结构体
在前面描述车辆和引擎的例子中,有时考虑些代码的便利性,会将结构体直接定义写在嵌入的结构体中。也就说,结构体的定义不会被外部引用到。在初始化这个被嵌入的结构体时,就需要再次声明结构体才能赋予数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | package main import "fmt" //车轮 type Wheel struct { Size int } //车 type Car struct { Wheel //引擎 Engine struct { Power int Type string } } func main() { c := Car{ //初始化车轮 Wheel: Wheel{ Size: 18, }, //初始化引擎 Engine: struct { Power int Type string }{ Type: "2.0T" , Power: 200, }, } fmt.Printf( "%+v\n" , c) } |
六、成员名字冲突
嵌入结构体内部可能拥有相同的成员名,成员重名时会发生什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package main import "fmt" type A struct { a int } type B struct { a int } type C struct { A B } func main() { c := &C{} // 等于 new(C) c.A.a = 1 fmt.Println(c) } |
运行结果输出:
1 2 3 4 5 6 7 | Starting: D:\ go -testfiles\bin\dlv.exe dap --check- go -version=false --listen=127.0.0.1:59329 from d:\ go -testfiles DAP server listening at: 127.0.0.1:59329 Type 'dlv help' for list of commands. &{{1} {0}} Process 18672 has exited with status 0 Detaching dlv dap (19704) exited with code: 0 |
接着,我们修改 main() 函数中如下代码:
1 2 3 4 5 | func main() { c := &C{} c.a = 1 fmt.Println(c) } |
此时再编译运行,编译器报错:
1 2 3 4 5 | Starting: D:\ go -testfiles\bin\dlv.exe dap --check- go -version=false --listen=127.0.0.1:59496 from d:\ go -testfiles DAP server listening at: 127.0.0.1:59496 Build Error: go build -o d:\ go -testfiles\__debug_bin.exe -gcflags all=-N -l .\结构体-类型内嵌和结构体内嵌-成员名字冲突. go # command-line-arguments .\结构体-类型内嵌和结构体内嵌-成员名字冲突. go :20:4: ambiguous selector c.a (exit status 2) |
编译器告知 C 的选择器 a 引起歧义,也就是说,编译无法决定将 1 赋值给 C 中的 A 还是 B 里字段的a。
在使用内嵌结构体时,Go 语言的编译器会非常智能地提醒我们可能发生的歧义和错误。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具