编程语言研究书库

组合: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. 可带走的

  1. 组合 = 把结构体嵌进结构体,Go 复用代码的标准姿势,取代继承;
  2. 嵌入写法:字段只写类型、不起名字;内层字段和方法都被提升到外层;
  3. 访问两条路:a.websiteinfo.url(全路径)和 a.url(提升后的直达),都合法;
  4. 方法 = 绑了接收者的函数:func (w T) 名() { ... },w 是方法体内指代被点名值的名字;
  5. 方法同样被提升:内层的方法能从外层的值上直接点出来;
  6. 外层方法里可以调内层方法(azn.basicInfo()),这是复用的日常形态;
  7. 指针嵌入 *T:外层共享内层实例、不复制;改一处处处可见;
  8. 方法要修改结构体,接收者必须是指针 *T;
  9. 字段可以是结构体切片,外层一对多管理内层(websitelist.websites);
  10. 组合是 HAS-A(有一份),继承是 IS-A(是一种)——Go 只给你前者。

9. 原文地图

主题原书章原文位置
组合的定义Compositiontext/20-fm-learning-go-programming.txt:49(搜「embedding of structs」)
websiteinfo/youtube/amazon 定义Compositiontext/20-fm-learning-go-programming.txt:61(搜「type websiteinfo struct」) · text/20-fm-learning-go-programming.txt:109(搜「youtube_var := youtube{」)
两种访问方式、字段提升Compositiontext/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 语法与提升Compositiontext/20-fm-learning-go-programming.txt:169(搜「func (webinfo websiteinfo) basicInfo」) · text/20-fm-learning-go-programming.txt:190(搜「amazon_var.basicInfo()」)
外层方法调内层方法Compositiontext/20-fm-learning-go-programming.txt:226(搜「azn.basicInfo()」)
完整例子的输出Compositiontext/20-fm-learning-go-programming.txt:379(搜「571 hours, 1 minute and 41 seconds」)
指针嵌入Compositiontext/20-fm-learning-go-programming.txt:421(搜「embedding a pointer to a struct」) · text/20-fm-learning-go-programming.txt:451(搜「* websiteinfo」)
嵌入切片与按索引更新Compositiontext/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」)
章末总结:值或引用嵌入、接口预告Compositiontext/20-fm-learning-go-programming.txt:674(搜「by values or by-passing reference」)

Footnotes

  1. 出处:「Composition」第 49 段(text/20-fm-learning-go-programming.txt:49,搜「embedding of structs」)。 ↩

  2. 出处:「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」)。 ↩

  3. 出处:「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」)。 ↩

  4. 出处:「Composition」第 166–184 段(text/20-fm-learning-go-programming.txt:169,搜「func (webinfo websiteinfo) basicInfo」)。 ↩

  5. 出处:「Composition」第 187–191 段(text/20-fm-learning-go-programming.txt:190,搜「amazon_var.basicInfo()」)。 ↩

  6. 出处:「Composition」第 223–241 段(text/20-fm-learning-go-programming.txt:226,搜「azn.basicInfo()」)。 ↩

  7. 出处:「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」)。 ↩

  8. 出处:「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」)。 ↩

On this page