第16章:结构体



结构体

和C语言类似,Go也支持结构体类型。此篇文章将介绍Go中的结构体类型和结构体值做一个详细的解释。

结构体类型和结构体字面量表示形式

每个无名结构体类型的字面形式均由struct关键字开头,后面跟着用一对大括号{},其中包裹着的一系列字段(field)声明。 一般来说,每个字段声明由一个字段名和字段类型组成。一个结构体类型的字段数目可以为0。下面是一个无名结构体类型的字面形式:

  1. struct {
  2. title string
  3. author string
  4. pages int
  5. }

上面这个结构体类型含有三个字段。前两个字段(title和author)的类型均为string。 最后一个字段pages的类型为int。

有时字段也称为成员变量。

相邻的同类型字段可以声明在一起。比如上面这个类型也可表示成下面这样:

  1. struct {
  2. title, author string
  3. pages int
  4. }

一个结构体类型的尺寸为它的所有字段的(类型)尺寸之和加上一些填充字节的数目。 常常地,编译器(和运行时)会在一个结构体值的两个相邻字段之间填充一些字节来保证一些字段的地址总是某个整数的倍数。 我们可以在后面的内存布局(第44章)一文中了解到字节填充(padding)和内存地址对齐(memory address alignment)。

一个零字段结构体的尺寸为零。

每个结构体字段在它的声明中可以被指定一个标签(tag)。从语法上讲,字段标签可以是任意字符串,它们是可选的,默认为空字符串。 但在实践中,它们应该被表示成用空格分隔的键值对形式,并且每个标签尽量使用直白字面形式(`…`)表示,而键值对中的值使用解释型字面形式(”…”)表示。 比如下例:

  1. struct {
  2. Title string `json:"title" myfmt:"s1"`
  3. Author string `json:"author,omitempty" myfmt:"s2"`
  4. Pages int `json:"pages,omitempty" myfmt:"n1"`
  5. X, Y bool `myfmt:"b1"`
  6. }

注意:上例中的X和Y字段的标签是一样的(尽管在实践中基本上从不会这样使用字段标签)。

我们可以使用反射(第27章)来检视字段的标签信息。

每个字段标签的目的取决于具体应用。上面这个例子中的字段标签用来帮助encoding/json标准库包来将上面这个结构体类型的某个值编码成JSON数据或者从一份JSON数据解码到上面这个结构体类型的某个值中。在编码和解码过程中,encoding/json标准库包中的函数将只考虑导出的结构体字段。这是为什么上面这个结构体的字段均为导出的。

把字段标签当成字段注释来使用不是一个好主意。

和C语言不一样,Go结构体不支持字段联合(union)。

上面的例子中展示的结构体类型都是无名的。在实践中,具名结构体类型用得更流行。

只有导出字段可以被使用在其它代码包中。非导出字段类以于很多其它语言中的私有或者保护型的成员变量。

一个结构体类型中的字段标签和字段的声明顺序对此结构体类型的身份识别很重要。 如果两个无名结构体类型的各个对应字段声明都相同(按照它们的出现顺序),则此两个无名结构体类型是等同的。 两个字段声明只有在它们的名称、类型和标签都等同的情况下才相同。 注意:两个声明在不同的代码包中的非导出字段将总被认为是不同的字段。

一个结构体类型不能(直接或者间接)含有一个类型为此结构类型的字段。

结构体字面量表示形式和结构体值的使用

在Go中,语法形式T{…}称为一个组合字面量形式(composite literal),其中T必须为一个类型名或者类型字面形式。 组合字面量形式可以用来表示结构体类型和内置容器类型(将在后面的文章中介绍)的值。

注意:组合字面量T{…}是一个类型确定值,它的类型为T。

假设S是一个结构体类型并且它的底层类型为struct{x int; y bool},S的零值可以表示成下面所示的组合字面量两种变种形式:

  1. S{0, false}。在此变种形式中,所有的字段名称均不出现,但每个字段的值必须指定,并且每个字段的出现顺序和它们的声明顺序必须一致。
  2. S{x: 0, y: false}、S{y: false, x: 0}、S{x: 0}、S{y: false}和S{}。 在此变种形式中,字段的名称和值必须成对出现,但是每个字段都不是必须出现的,并且字段的出现顺序并不重要。 没有出现的字段的值被编译器认为是它们各自类型的零值。S{}是最常用的类型S的零值的表示形式。

如果S是声明在另一个代码包中的一个结构体类型,则推荐使用上面所示的第二种变种形式来表示它的值。 因为另一个代码包的维护者今后可能会在此结构体中添加新的字段,从而导致当前使用的第一种变种形式在今后可能编译不通过。

当然,上面所示的结构体值的组合字面量也可以用来表示结构体类型的非零值。

对于类型S的一个值v,我们可以用v.x和v.y来表示它的字段。 v.x(或v.y)这种形式称为一个选择器(selector)。其中的v称为此选择器的属主。 今后,我们称一个选择器中的句点.为属性选择操作符。

一个例子:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Book struct {
  6. title, author string
  7. pages int
  8. }
  9. func main() {
  10. book := Book{"Go语言101", "老貘", 256}
  11. fmt.Println(book) // {Go语言101 老貘 256}
  12. // 使用带字段名的组合字面量来表示结构体值。
  13. book = Book{author: "老貘", pages: 256, title: "Go语言101"}
  14. // title和author字段的值都为空字符串"",pages字段的值为0。
  15. book = Book{}
  16. // title字段空字符串"",pages字段为0。
  17. book = Book{author: "老貘"}
  18. // 使用选择器来访问和修改字段值。
  19. var book2 Book // <=> book2 := Book{}
  20. book2.author = "Tapir"
  21. book2.pages = 300
  22. fmt.Println(book2.pages) // 300
  23. }

如果一个组合字面量中最后一项和结尾的}处于同一行,则此项后的逗号,是可选的;否则此逗号不可省略。 我们可以阅读后面的Go代码断行规则(第28章)一文了解更多断行规则。

  1. var _ = Book {
  2. author: "老貘",
  3. pages: 256,
  4. title: "Go语言101", // 这里行尾的逗号不可省略
  5. }
  6. // 下行}前的逗号可以省略。
  7. var _ = Book{author: "老貘", pages: 256, title: "Go语言101",}

关于结构体值的赋值

当一个(源)结构体值被赋值给另外一个(目标)结构体值时,其效果和逐个将源结构体值的各个字段赋值给目标结构体值的各个对应字段的效果是一样的。

  1. func f() {
  2. book1 := Book{pages: 300}
  3. book2 := Book{"Go语言101", "老貘", 256}
  4. book2 = book1
  5. // 上面这行和下面这三行是等价的。
  6. book2.title = book1.title
  7. book2.author = book1.author
  8. book2.pages = book1.pages
  9. }

如果两个结构体值的类型不同,则只有在它们的底层类型相同(要考虑字段标签)并且其中至少有一个结构体值的类型为无名类型(第14章)时(换句话说,只有它们可以被隐式转换为对方的类型的时候,见下)才可以互相赋值。

结构体字段的可寻址性

如果一个结构体值是可寻址的,则它的字段也是可寻址的;反之,一个不可寻址的结构体值的字段也是不可寻址的。 不可寻址的字段的值是不可更改的。所有的组合字面量都是不可寻址的。

一个例子:

  1. package main
  2. import "fmt"
  3. func main() {
  4. type Book struct {
  5. Pages int
  6. }
  7. var book = Book{} // 变量值book是可寻址的
  8. p := &book.Pages
  9. *p = 123
  10. fmt.Println(book) // {123}
  11. // 下面这两行编译不通过,因为Book{}是不可寻址的,
  12. // 继而Book{}.Pages也是不可寻址的。
  13. /*
  14. Book{}.Pages = 123
  15. p = &Book{}.Pages // <=> p = &(Book{}.Pages)
  16. */
  17. }

注意:选择器中的属性选择操作符.的优先级比取地址操作符&的优先级要高。

组合字面量不可寻址但可被取地址

一般来说,只有可被寻址的值才能被取地址,但是Go中有一个语法糖(语法例外):虽然所有的组合字面量都是不可寻址的,但是它们都可被取地址。

例子:

  1. package main
  2. func main() {
  3. type Book struct {
  4. Pages int
  5. }
  6. // Book{100}是不可寻址的,但是它可以被取地址。
  7. p := &Book{100} // <=> tmp := Book{100}; p := &tmp
  8. p.Pages = 200
  9. }

在字段选择器中,属主结构体值可以是指针,它将被隐式解引用

比如,在下面的例子中,为了简洁,(*bookN).pages可以被写成bookN.pages。 换句话说,在这种简写形式中,bookN将被隐式解引用。

  1. package main
  2. func main() {
  3. type Book struct {
  4. pages int
  5. }
  6. book1 := &Book{100} // book1是一个指针
  7. book2 := new(Book) // book2是另外一个指针
  8. // 像使用结构值一样来使用结构体值的指针。
  9. book2.pages = book1.pages
  10. // 上一行等价于下一行。换句话说,上一行
  11. // 两个选择器中的指针属主将被自动解引用。
  12. (*book2).pages = (*book1).pages
  13. }

关于结构体值的比较

如果一个结构体类型是可比较的,则它肯定不包含不可比较类型(第14章)的字段(这里不忽略名为空标识符_的字段)。

和结构体值的赋值规则类似,如果两个不同类型的结构体值均为可比较的,则它们仅在它们的底层类型相同(要考虑字段标签)并且其中至少有一个结构体值的类型为无名类型时(换句话说,只有它们可以被隐式转换为对方的类型的时候,见下)才可以互相比较。

如果两个结构体值可以相互比较,则它们的比较结果等同于逐个比较它们的相应字段(按照字段在代码中的声明顺序)。 两个结构体值只有在它们的相应字段都相等的情况下才相等;当一对字段被发现不相等的或者在比较中产生恐慌(第23章)的时候,对结构体的比较将提前结束结束。 在比较中,名为空标识符_的字段将被忽略掉。

关于结构体值的类型转换

两个类型分别为S1和S2的结构体值只有在S1和S2的底层类型相同(忽略掉字段标签)的情况下才能相互转换为对方的类型。 特别地,如果S1和S2的底层类型相同(要考虑字段标签)并且只要它们其中有一个为无名类型,则此转换可以是隐式的。

比如,对于下面的代码片段中所示的五个结构体类型:S0、S1、S2、S3和S4:

  • 类型S0的值不能被转换为其它四个类型中的任意一个,原因是它与另外四个类型的对应字段名不同(因此底层类型不同)。
  • 类型S1、S2、S3和S4的任意两个值可以转换为对方的类型。

特别地,

  • S2表示的类型的值可以被隐式转化为类型S3,反之亦然。
  • S2表示的类型的值可以被隐式转换为类型S4,反之亦然。

但是,

  • S2表示的类型的值必须被显式转换为类型S1,反之亦然。
  • 类型S3的值必须被显式转换为类型S4,反之亦然。
  1. package main
  2. type S0 struct {
  3. y int "foo"
  4. x bool
  5. }
  6. type S1 = struct { // S1是一个无名类型
  7. x int "foo"
  8. y bool
  9. }
  10. type S2 = struct { // S2也是一个无名类型
  11. x int "bar"
  12. y bool
  13. }
  14. type S3 S2 // S3是一个定义类型(因而具名)。
  15. type S4 S3 // S4是一个定义类型(因而具名)。
  16. // 如果不考虑字段标签,S3(S4)和S1的底层类型一样。
  17. // 如果考虑字段标签,S3(S4)和S1的底层类型不一样。
  18. var v0, v1, v2, v3, v4 = S0{}, S1{}, S2{}, S3{}, S4{}
  19. func f() {
  20. v1 = S1(v2); v2 = S2(v1)
  21. v1 = S1(v3); v3 = S3(v1)
  22. v1 = S1(v4); v4 = S4(v1)
  23. v2 = v3; v3 = v2 // 这两个转换可以是隐式的
  24. v2 = v4; v4 = v2 // 这两个转换也可以是隐式的
  25. v3 = S3(v4); v4 = S4(v3)
  26. }

事实上,两个结构体值只有在它们可以相互隐式转换为对方的类型的时候才能相互赋值和比较。

匿名结构体类型可以使用在结构体字段声明中

匿名结构体类型允许出现在结构体字段声明中。匿名结构体类型也允许出现在组合字面量中。

一个例子:

  1. var aBook = struct {
  2. author struct { // 此字段的类型为一个匿名结构体类型
  3. firstName, lastName string
  4. gender bool
  5. }
  6. title string
  7. pages int
  8. }{
  9. author: struct {
  10. firstName, lastName string
  11. gender bool
  12. }{
  13. firstName: "Mark",
  14. lastName: "Twain",
  15. }, // 此组合字面量中的类型为一个匿名结构体类型
  16. title: "The Million Pound Note",
  17. pages: 96,
  18. }

通常来说,为了代码可读性,最好少使用匿名结构体类型。

更多关于结构体类型

Go中有一些和结构体类型相关的进阶知识点。这些知识点将后面的类型内嵌(第24章)和内存布局(第44章)两篇文章中介绍。


本书由老貘历时三年写成。目前本书仍在不断改进和增容中。你的赞赏是本书和Go101.org网站不断增容和维护的动力。

(请搜索关注微信公众号“Go 101”或者访问github.com/golang101/golang101获取本书最新版)