组合:Go 取代继承的那套搭法,以及方法的首次登场
组合:Go 取代继承的那套搭法,以及方法的首次登场
这一章回答「Go 没有继承,代码怎么复用」。 答案是组合(composition): 把一个结构体嵌进另一个结构体,内层的字段和方法一起归外层使用。 主线例子是三个类型:通用的 websiteinfo(网站的公共信息), 嵌着它的 youtube 和 amazon——它们没有复制任何字段,却「拥有」了全部公共能力。 这一章还迎来了全书第一次方法登场,读懂它,第 13 章的接口就顺水推舟。
1. 组合是什么
书里给的定义:组合就是把结构体嵌进结构体,把小而专的类型拼成大的应用; 它换来的是可读性和灵活性1。
对应到主线例子的形状:
websiteinfo ← 通用件:name, url, ageinyear
╱ ╲
youtube amazon ← 专用件:各自嵌一个 websiteinfo
ytFirstVideo departments
ytlongestvideo can_sell
图说:websiteinfo 是公共底座;两个具体类型「嵌入」它,
而不是「继承」它。没有父子,只有拼装。2. 主线例子:websiteinfo 的一生
照原书完整走一遍2。
第 ① 步:定义通用件和两个使用者。
type websiteinfo struct {
name, url string
ageinyear int
}
type youtube struct {
websiteinfo // 嵌入:只有类型,没有字段名
ytFirstVideo string
ytlongestvideo string
}
type amazon struct {
websiteinfo // 同一个通用件,嵌第二次
departments string
can_sell bool
}第 ② 步:初始化。 嵌入字段在字面量里占一格,值就是那个内层结构体:
youtube_var := youtube{
websiteinfo{"Youtube", "www.youtube.com", 14}, // 内层整块
"me at the zoo",
"571 hours, 1 minute and 41 seconds",
}第 ③ 步:访问——两条路都通。 这是嵌入的核心效果,书里并排演示了两种写法,
输出完全相同(www.youtube.com 打两遍)3:
fmt.Println(youtube_var.websiteinfo.url) // 老路:先点进内层,再点字段
fmt.Println(youtube_var.url) // 新路:字段被提升,一层直达图说:嵌入的字段被「提升」到外层。url 本来是 websiteinfo 的字段,
嵌入后好像直接长在 youtube 身上。被提升的不只是字段——还有方法。
3. 方法的首次登场:receiver 语法
组合讲到一半,书里自然地引入了方法。先给定义:
方法是绑在类型上的函数:调用时通过某个具体值来点它,那个值叫接收者(receiver)。 声明语法和函数只差一个括号里的接收者:
func (webinfo websiteinfo) basicInfo() {
fmt.Println("Name of website:", webinfo.name)
fmt.Println("Url of website:", webinfo.url)
fmt.Println("How old is this website:", webinfo.ageinyear)
}func (webinfo websiteinfo) 读作:「这是 websiteinfo 类型的方法,
方法体内用 webinfo 这个名字指代被点名的那个值。」
之后的 basicInfo() 和函数体写法没有任何区别4。
方法也会被提升。 因为 youtube 嵌入了 websiteinfo, 所以 websiteinfo 的方法 basicInfo 可以直接从 youtube 的值上调用—— 书里给了两种等价写法5:
youtube_var.websiteinfo.basicInfo() // 老老实实点进去
amazon_var.basicInfo() // 提升后的直达写法在外层方法里调内层方法也一样自然——amazon 的 amazonInfo 里直接
azn.basicInfo(),先把公共信息打出来,再打自己的字段6:
func (azn amazon) amazonInfo() {
azn.basicInfo() // 复用内层方法
fmt.Println("Departments of amazon:", azn.departments)
fmt.Println("Can we sell on amazon:", azn.can_sell)
}这就是「代码复用」在 Go 里的样子:公共的数据和方法住在小结构体里, 谁需要就嵌谁;外层的方法可以像调自己的方法一样调内层的方法。
判断(我们的,不是书里的): 组合和继承都能复用代码,差别在「关系」的形状: 继承是「youtube 是一种 websiteinfo」(IS-A),组合是「youtube 有一份 websiteinfo」(HAS-A)。 Go 押注 HAS-A 的理由,书里第 1 章已经给过一半:继承层级深了好读难改, 组合层级永远是平的。另一半书里没讲:嵌入只是语法糖, 编译器把它翻译成普通字段 + 一层自动转发——所以它没有继承那套 虚函数表、多态链的开销和复杂性。代价:内层方法不知道「外层是谁」, 想做多态,得靠下一章的接口。 如果错,会错在: 深组合(嵌三四层)同样有「字段从哪来」的迷失问题, HAS-A 不是万能药;但那是使用纪律问题,不是机制缺陷。
4. 指针嵌入:共享同一个实例
嵌入字段也可以是指针:*websiteinfo。外层不复制内层,只存一个指向内层的门牌号。
type youtube struct {
*websiteinfo // 指针嵌入
ytFirstVideo string
}
webinfo := &websiteinfo{"YouTube", "www.youtube.com", 14}
youtube_var := youtube{
websiteinfo: webinfo, // 把同一个实例的地址塞进去
ytFirstVideo: "Me at the zoo",
}
youtube_var.BasicInfo() // 方法照常可用书里强调的动机:普通嵌入会把内层整个复制一份;指针嵌入让你引用一个 特定的实例,避免复制7。多个外层结构体可以嵌同一个内层实例—— 改一处,处处可见(这正是共享,也是风险,和切片共享底层数组一个道理)。
一个细节:指针嵌入时,方法接收者也常常写成指针
(func (webinfo *websiteinfo) BasicInfo())。书里在这个例子里首字母大写了
BasicInfo——顺带复习:大写 = 导出。
5. 嵌入切片:一对多的聚合
结构体的字段可以是一个结构体切片——外层管理一批内层实例:
type websitelist struct {
websites []websiteinfo // 一批 websiteinfo
}
func (wl *websitelist) updateWebsiteAge(index int, newAge int) {
if index >= 0 && index < len(wl.websites) {
wl.websites[index].ageinyear = newAge
}
}主线例子的收尾:三个网站 YouTube(14 岁)、Google(25)、Facebook(20),
调 updateWebsiteAge(0, 15) 之后,YouTube 的年龄变 15,其余不动8。
注意接收者是 *websitelist(指针):方法要改结构体的字段,接收者就得是指针——
这和「函数想改外面的变量要传指针」是同一条规矩(书里此前的方法都是值接收者、只读)。
6. 作者的判断与证据
| 书里的说法 | 性质 |
|---|---|
| 「组合提供可读性和灵活性」 | 设计主张,Go 官方立场一致 |
| 「嵌入后外层可以访问内层全部字段和方法」 | 机制事实;「全部」要打折——只能访问导出的(大写)部分,以及同包内的小写部分 |
| 「Go 用组合代替继承」 | 语言设计事实,Go FAQ 明载 |
7. 边界与局限
- 方法提升有一个书里没讲的坑:内层方法里的接收者仍是内层类型——
basicInfo里拿到的 webinfo 只是外层里的那一小块, 它看不见外层的 ytFirstVideo。想「让内层方法知道外层」,需要接口(第 13 章); - 冲突规则未展开:两个嵌入字段有同名字段时,该字段不会被提升,
访问必须写全路径(如
a.websiteinfo1.url与a.websiteinfo2.url); - 嵌入切片严格说不是「嵌入」(它有字段名 websites),是普通的切片字段, 书里把它归入组合家族,本书沿用这个口径,但读者要分清两者的语法差别。
8. 可带走的
- 组合 = 把结构体嵌进结构体,Go 复用代码的标准姿势,取代继承;
- 嵌入写法:字段只写类型、不起名字;内层字段和方法都被提升到外层;
- 访问两条路:
a.websiteinfo.url(全路径)和a.url(提升后的直达),都合法; - 方法 = 绑了接收者的函数:
func (w T) 名() { ... },w 是方法体内指代被点名值的名字; - 方法同样被提升:内层的方法能从外层的值上直接点出来;
- 外层方法里可以调内层方法(
azn.basicInfo()),这是复用的日常形态; - 指针嵌入
*T:外层共享内层实例、不复制;改一处处处可见; - 方法要修改结构体,接收者必须是指针
*T; - 字段可以是结构体切片,外层一对多管理内层(
websitelist.websites); - 组合是 HAS-A(有一份),继承是 IS-A(是一种)——Go 只给你前者。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 组合的定义 | Composition | text/20-fm-learning-go-programming.txt:49(搜「embedding of structs」) |
| websiteinfo/youtube/amazon 定义 | Composition | text/20-fm-learning-go-programming.txt:61(搜「type websiteinfo struct」) · text/20-fm-learning-go-programming.txt:109(搜「youtube_var := youtube{」) |
| 两种访问方式、字段提升 | Composition | text/20-fm-learning-go-programming.txt:124(搜「youtube_var.websiteinfo.url」) · text/20-fm-learning-go-programming.txt:127(搜「youtube_var.url」) · text/20-fm-learning-go-programming.txt:163(搜「directly access the fields」) |
| 方法 receiver 语法与提升 | Composition | text/20-fm-learning-go-programming.txt:169(搜「func (webinfo websiteinfo) basicInfo」) · text/20-fm-learning-go-programming.txt:190(搜「amazon_var.basicInfo()」) |
| 外层方法调内层方法 | Composition | text/20-fm-learning-go-programming.txt:226(搜「azn.basicInfo()」) |
| 完整例子的输出 | Composition | text/20-fm-learning-go-programming.txt:379(搜「571 hours, 1 minute and 41 seconds」) |
| 指针嵌入 | Composition | text/20-fm-learning-go-programming.txt:421(搜「embedding a pointer to a struct」) · text/20-fm-learning-go-programming.txt:451(搜「* websiteinfo」) |
| 嵌入切片与按索引更新 | Composition | text/20-fm-learning-go-programming.txt:553(搜「websites []websiteinfo」) · text/20-fm-learning-go-programming.txt:624(搜「updateWebsiteAge(0, 15)」) · text/20-fm-learning-go-programming.txt:662(搜「Age: 15」) |
| 章末总结:值或引用嵌入、接口预告 | Composition | text/20-fm-learning-go-programming.txt:674(搜「by values or by-passing reference」) |
Footnotes
-
出处:「Composition」第 49 段(
text/20-fm-learning-go-programming.txt:49,搜「embedding of structs」)。 ↩ -
出处:「Composition」第 55–160 段(
text/20-fm-learning-go-programming.txt:61,搜「type websiteinfo struct」;text/20-fm-learning-go-programming.txt:109,搜「youtube_var := youtube{」;text/20-fm-learning-go-programming.txt:145,搜「571 hours」)。 ↩ -
出处:「Composition」第 121–151 段(
text/20-fm-learning-go-programming.txt:124,搜「youtube_var.websiteinfo.url」;text/20-fm-learning-go-programming.txt:127,搜「youtube_var.url」;text/20-fm-learning-go-programming.txt:148,搜「me at the zoo」)。 ↩ -
出处:「Composition」第 166–184 段(
text/20-fm-learning-go-programming.txt:169,搜「func (webinfo websiteinfo) basicInfo」)。 ↩ -
出处:「Composition」第 187–191 段(
text/20-fm-learning-go-programming.txt:190,搜「amazon_var.basicInfo()」)。 ↩ -
出处:「Composition」第 223–241 段(
text/20-fm-learning-go-programming.txt:226,搜「azn.basicInfo()」)。 ↩ -
出处:「Composition」第 418–517 段(
text/20-fm-learning-go-programming.txt:421,搜「embedding a pointer to a struct」;text/20-fm-learning-go-programming.txt:487,搜「websiteinfo: webinfo」)。 ↩ -
出处:「Composition」第 520–668 段(
text/20-fm-learning-go-programming.txt:553,搜「websites []websiteinfo」;text/20-fm-learning-go-programming.txt:624,搜「updateWebsiteAge(0, 15)」;text/20-fm-learning-go-programming.txt:662,搜「Age: 15」)。 ↩