跳转至

结构体:Go 世界里的「类」,以及嵌套与嵌入之分

这一章讲 Go 里最重要的自定义类型:结构体。 它是 Go 对「类」的回答——有数据、能打包,但不带继承。 读完你会:熟练声明和初始化结构体,分清「嵌套」和「嵌入」这两个一字之差、 机制完全不同的概念(后者是 Go 组合式设计的原子),并掌握结构体的值语义。

1. 结构体是什么

书里给的定义:结构体是用户定义的复合数据类型,把若干不同类型的变量 定义在同一块内存里,合成一个类型1。

书里还说了一句定位的话:struct 在 Go 里的角色和面向对象语言里的类相同, 区别是类支持继承,结构体支持组合2——这句话是第 11、12 两章的总纲。

声明用 type ... struct:

type student struct {
    name, city string   // 同类型字段可以合并写
    rollno     int
}

type 关键字告诉编译器:后面是一个新类型。每个字段是「名字 + 类型」, 也可以不写名字只写类型(见第 5 节匿名结构体)。

2. 主线例子:一个 student 的完整一生

本章主线,照原书的 student 走完全程。

出生——声明变量。 结构体变量的零值是「每个字段各取零值」:

var s student
fmt.Println(s)   // → {  0}    字符串是空、整数是 0

零值的规律和第 3 章一致:Go 不会给你留随机数据,新结构体永远是干净的3。

落户——初始化,两种姿势。

姿势一,按位置给值(顺序必须和字段声明一致):

s = student{"rahul", "jaipur", 67}
fmt.Println(s)   // → {rahul jaipur 67}

书里给过警告:顺序不对,编译错4。姿势二,按字段名给值(推荐):

s = student{rollno: 67, city: "jaipur"}   // 没给的字段取零值
// → { jaipur 67}

按名字初始化可以乱序、可以漏字段——可读性和可维护性都更好,日常写码默认用这种4。

日常使用——点号访问字段,结构体是可变的:

s.name = "rahul"
s.rollno = 67

点号既能读也能写,所以书里说 struct 是 mutable(可变的)—— 注意这和字符串的不可变形成对照:结构体本身可以改,字符串不能5。

复制与传参——值语义:

student_copy := s          // 整份复制
getValue(&student_copy)    // 想让函数改它?传地址

书里演示了 getValue(student_copy1 *student) 里改 student_copy1.name = "Manish", 外面的副本跟着变——和拆解 09 的指针改值是同一个机制6。

比较——可判等:

student{...rollno: 11} == student{...rollno: 11}   // true
student{...rollno: 11} == student{...rollno: 0}    // false
// {rahul jaipur 0} == {rahul jaipur}              // true,零值补全后相等

两个结构体相等 = 类型相同 + 每个字段的值都相等;漏写的字段按零值参与比较7。

3. 指针与结构体:new 和一个语法糖

创建指向结构体的指针有两条路,书里都给了8:

s := student{"rahul", "jaipur", 67}
p1 := &s                       // 先造变量,再取地址

p2 := &student{"rahul", "jaipur", 67}   // 一步到位
p3 := new(student)                        // new 分配内存,字段全是零值

new(student) 返回的就是 *student——new 永远返回指针, p3.city = "jaipur" 直接改的是那块新内存9。

访问字段有一个重要的语法糖,书里并排演示了两种写法:

fmt.Println((*p1).city)   // 严格写法:先解引用,再取字段
fmt.Println(p1.city)      // 语法糖:直接写,Go 自动帮你进屋

对结构体指针用点号,Go 自动解引用——所以日常代码里几乎见不到 (*p).field 的写法。 (注意:这个糖只对结构体有效;普通变量的指针仍要写 *p。)

4. 嵌套 vs 嵌入:一字之差,两个机制

这是本章最关键的一对概念,书里用同一个 result(数学/科学/英语成绩+是否及格) 放在 student 里做了两种版本10:

嵌套(nested)——内层结构体是外层的一个普通字段,有名字:

type student struct {
    name, city string
    rollno     int
    result     result        // 字段名就叫 result,类型也叫 result
}

a := student{..., result: result{45, 78, 90, true}}
fmt.Println(a.result.isPass)   // 访问要过两层:外层字段名.内层字段名

嵌入(embedded)——内层结构体不带字段名,字段被「提升」:

type student struct {
    name, city string
    rollno     int
    result                     // 只有类型,没有字段名——这就是嵌入
}

a := student{"rahul", "jaipur", 11, result{45, 78, 90, true}}
fmt.Println(a.isPass)          // 直接访问内层字段!不用写 a.result.isPass

图说:嵌套是「书包里装着一个文具盒」,拿笔要开两层; 嵌入是「文具盒的格子直接长在书包上」,拿笔只要开一层。 书里把嵌入的效果叫字段提升(promote):内层字段被提升为外层可直接访问的字段10。

嵌入就是 Go 的「组合式继承」:第 12 章会讲,嵌进去的结构体的能力 (字段、方法)整个归外层使用——这就是 Go 没有 class 继承之后, 复用代码的标准姿势。

判断(我们的,不是书里的): 「嵌入」是 Go 最被低估的设计之一: 语法上只是少写一个字段名,机制上却给了完整的代码复用。 但书里也点到它的代价——命名冲突:外层和内层有同名字段时, 外层的优先,内层的被遮蔽,想访问内层得写全 a.result.isPass。 嵌入层级多了之后,字段「从哪一层来」会变难追,嵌两层以上要慎重。 如果错,会错在: 有些团队(包括 Go 官方的部分代码风格讨论)主张 只用嵌入做「满足接口」的委托,不拿它共享数据——这是风格之争, 嵌入的机制能力本身不受影响。

5. 匿名结构体与匿名字段

匿名结构体:不为结构体起类型名,直接声明变量,一次性的形状用它:

var student1 struct {
    name, city string
    rollno     int
}

它和有名字的 struct 完全同权:能初始化、能比较、能取指针11。

匿名字段:字段不给名字,只写类型,类型名就是字段名:

type student struct {
    string      // 字段名就是 "string"
    int         // 字段名就是 "int"
}

a := student{"rahul", 67}
fmt.Println(a)   // → {rahul 67}

规矩:一个结构体里每种类型只能匿名一次——两个 string 匿名字段, 编译错 string redeclared;同类型的第二个字段必须起名字12。

6. 作者的判断与证据

书里的说法 性质
「struct 类似类,区别是继承 vs 组合」 准确的定位,Go 官方 FAQ 的口径
「嵌入更干净,但可能引起命名冲突」 事实陈述,克制且属实

7. 边界与局限

  • 方法没有登场:给结构体绑函数(方法)是第 13 章接口和第 12 章组合的前置, 本章的结构体只有数据。看完第 12 章再回头,struct 才是完整的「类替代品」;
  • 结构体标签(tag,如 JSON 用的反引号标记)本章没讲——序列化(把内存里的 数据转成可存储、可传输的文本)和反序列化都靠它,那是结构体 在真实项目里的高频用法(补充,不在书里);
  • 嵌入的指针形式(嵌入 *result)、多层嵌入的冲突规则本章未展开,第 12 章会补指针嵌入。

8. 可带走的

  1. 结构体 = 若干字段捆成一个类型;type T struct { 字段 类型 ... };
  2. 结构体变量的零值 = 每个字段各取零值,永远是干净的;
  3. 初始化两种姿势:按位置(顺序必须对)和按字段名(推荐,可乱序可漏);
  4. 点号读写字段,结构体可变;字符串不可变——对照记;
  5. 结构体是值类型:赋值复制整份;函数想改它,传 &s;
  6. 指向结构体的指针:&T{...} 或 new(T);指针用点号访问字段时 Go 自动解引用;
  7. 嵌套 vs 嵌入:嵌套要过两层(a.result.isPass),嵌入字段提升一层直达(a.isPass);
  8. 嵌入 = Go 组合式继承的原子,代价是可能命名冲突;
  9. 匿名结构体一次性使用;匿名字段类型即字段名,每种类型只能匿名一次;
  10. 结构体可判等:类型相同 + 逐字段相等(漏写按零值)。

9. 原文地图

主题 原书章 原文位置
结构体定义、类与组合的区分 Structures text/19-fm-learning-go-programming.txt:64(搜「composite data type」) · text/19-fm-learning-go-programming.txt:994(搜「classes support inheritance」)
声明与零值 Structures text/19-fm-learning-go-programming.txt:100(搜「type student struct」) · text/19-fm-learning-go-programming.txt:178(搜「{ 0}」)
两种初始化 Structures text/19-fm-learning-go-programming.txt:229(搜「student{"rahul", "jaipur", 67}」) · text/19-fm-learning-go-programming.txt:262(搜「rollno:67」)
点号访问与可变性 Structures text/19-fm-learning-go-programming.txt:322(搜「student.city」)
指针与结构体、自动解引用 Structures text/19-fm-learning-go-programming.txt:385(搜「p1 := &student_var」) · text/19-fm-learning-go-programming.txt:394(搜「(*p1).city」)
new 与结构体 Structures text/19-fm-learning-go-programming.txt:475(搜「new(student)」) · text/19-fm-learning-go-programming.txt:487(搜「{ jaipur 67}」)
匿名字段与重复报错 Structures text/19-fm-learning-go-programming.txt:514(搜「type as the name」) · text/19-fm-learning-go-programming.txt:571(搜「string redeclared」)
嵌套结构体 Structures text/19-fm-learning-go-programming.txt:646(搜「Nested struct as a field」) · text/19-fm-learning-go-programming.txt:685(搜「a.result.isPass」)
嵌入结构体与字段提升 Structures text/19-fm-learning-go-programming.txt:730(搜「11, result{45, 78, 90, true}」) · text/19-fm-learning-go-programming.txt:733(搜「{rahul jaipur 11 {45 78 90 true}}」)
复制、传地址给函数 Structures text/19-fm-learning-go-programming.txt:811(搜「new copy is created」) · text/19-fm-learning-go-programming.txt:868(搜「getValue(&student_copy)」)
结构体比较 Structures text/19-fm-learning-go-programming.txt:934(搜「student==student1」) · text/19-fm-learning-go-programming.txt:985(搜「student3==student4」)

  1. 出处:「Structures」第 64 段(text/19-fm-learning-go-programming.txt:64,搜「composite data type」)。 ↩

  2. 出处:「Structures」第 994 段(text/19-fm-learning-go-programming.txt:994,搜「classes support inheritance」)。 ↩

  3. 出处:「Structures」第 175–199 段(text/19-fm-learning-go-programming.txt:178,搜「{ 0}」;text/19-fm-learning-go-programming.txt:199,搜「default values for every field」)。 ↩

  4. 出处:「Structures」第 229–265 段(text/19-fm-learning-go-programming.txt:229,搜「student{"rahul", "jaipur", 67}」;text/19-fm-learning-go-programming.txt:238,搜「same order」;text/19-fm-learning-go-programming.txt:262,搜「rollno:67」)。 ↩↩

  5. 出处:「Structures」第 301–352 段(text/19-fm-learning-go-programming.txt:328,搜「{ jaipur 67}」)。 ↩

  6. 出处:「Structures」第 808–871 段(text/19-fm-learning-go-programming.txt:811,搜「new copy is created」;text/19-fm-learning-go-programming.txt:829,搜「Manish」;text/19-fm-learning-go-programming.txt:868,搜「getValue(&student_copy)」)。 ↩

  7. 出处:「Structures」第 874–985 段(text/19-fm-learning-go-programming.txt:934,搜「student==student1」;text/19-fm-learning-go-programming.txt:985,搜「student3==student4」)。 ↩

  8. 出处:「Structures」第 358–448 段(text/19-fm-learning-go-programming.txt:385,搜「p1 := &student_var」;text/19-fm-learning-go-programming.txt:403,搜「p2 := &student」)。 ↩

  9. 出处:「Structures」第 451–508 段(text/19-fm-learning-go-programming.txt:475,搜「new(student)」;text/19-fm-learning-go-programming.txt:487,搜「{ jaipur 67}」)。 ↩

  10. 出处:「Structures」第 604–805 段(text/19-fm-learning-go-programming.txt:607,搜「promotes their fields」;text/19-fm-learning-go-programming.txt:685,搜「a.result.isPass」;text/19-fm-learning-go-programming.txt:733,搜「{rahul jaipur 11 {45 78 90 true}}」)。 ↩↩

  11. 出处:「Structures」第 133–199 段(text/19-fm-learning-go-programming.txt:133,搜「anonymous structure」)。 ↩

  12. 出处:「Structures」第 511–598 段(text/19-fm-learning-go-programming.txt:571,搜「string redeclared」;text/19-fm-learning-go-programming.txt:598,搜「{rahul jaipur 67}」)。 ↩