结构体

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

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

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

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

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

相邻的同类型字段可以声明在一起。比如上面这个类型也可表示成下面这样:
struct {
	title, author string
	pages         int
}

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

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

每个结构体字段在它的声明中可以被指定一个标签(tag)。字段标签可以是任意字符串,它们是可选的,默认为空字符串。 下面这个结构体类型中的字段均被指定了标签。
struct {
	Title  string `json:"title"`
	Author string `json:"author,omitempty"`
	Pages  int    `json:"pages,omitempty"`
}

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

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

在实践中,在指定字段标签时,字符串直白字面表示(`...`)比解释型字面表示("...")用得更流行一些。

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

上面的例子中展示的结构体类型都是非定义的和匿名的。在实践中,定义的结构体类型用得更流行。

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

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

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

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

在Go中,语法形式T{...}称为一个组合字面形式(composite literal),其中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.xv.y来表示它的字段。 v.x这种形式称为一个选择器(selector)。 今后,我们称一个选择器中的句点.为属性选择操作符。

一个例子:
package main

import (
	"fmt"
)

type Book struct {
	title, author string
	pages         int
}

func main() {
	book := Book{"Go语言101", "老貘", 256}
	fmt.Println(book) // {Go语言101 老貘 256}

	// 使用带字段名的组合字面形式来表示结构体值。
	book = Book{author: "老貘", pages: 256, title: "Go语言101"}
	// title和author字段的值都为空字符串"",pages字段的值为0。
	book = Book{}
	// title字段空字符串"",pages字段为0。
	book = Book{author: "老貘"}

	// 使用选择器来访问和修改字段值。
	var book2 Book // <=> book2 := Book{}
	book2.author = "Tapir"
	book2.pages = 300
	fmt.Println(book.pages) // 300
}

如果一个组合字面形式中最后一项和结尾的}处于同一行,则此项后的逗号,是可选的;否则此逗号不可省略。 我们可以阅读后面的Go代码断行规则一文了解更多断行规则。
var _ = Book {
	author: "老貘",
	pages: 256,
	title: "Go语言101", // 这里行尾的逗号不可省略
}

// 下行}前的逗号可以省略。
var _ = Book{author: "老貘", pages: 256, title: "Go语言101",}

关于结构体值的赋值

当一个(源)结构体值被赋值给另外一个(目标)结构体值时,其效果和逐个将源结构体值的各个字段赋值给目标结构体值的各个对应字段的效果是一样的。
func f() {
	book1 := Book{pages: 300}
	book2 := Book{"Go语言101", "老貘", 256}

	book2 = book1
	// 上面这行和下面这三行是等价的。
	book2.title = book1.title
	book2.author = book1.author
	book2.pages = book1.pages
}

两个结构体值只有在它们的类型的底层类型一样(要考虑字段标签)并且其中至少有一个结构体值的类型为非定义类型时才可以互相赋值。

结构体字段的可寻址性

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

一个例子:
package main

import "fmt"

func main() {
	type Book struct {
		Pages int
	}
	var book = Book{} // 变量值book是可寻址的
	p := &book.Pages
	*p = 123
	fmt.Println(book) // {123}

	// 下面这两行编译不通过,因为Book{}.Pages是不可寻址的。
	/*
	Book{}.Pages = 123
	p = &Book{}.Pages // <=> p = &(Book{}.Pages)
	*/
}

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

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

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

例子:
package main

func main() {
	type Book struct {
		Pages int
	}
	// Book{100}是不可寻址的,但是它可以被取地址。
	p := &Book{100} // <=> tmp := Book{100}; p := &tmp
	p.Pages = 200
}

在选择器中,结构体值的指针可以当作结构值来使用

和C语言不同,Go中没有->操作符用来通过一个结构体值的指针来访为此结构体值的字段。 在Go中,->操作符也是用句点.来表示的。

一个例子:
package main

func main() {
	type Book struct {
		pages int
	}
	book1 := &Book{100} // book1是一个指针
	book2 := new(Book)  // book2是另外一个指针
	// 像使用结构值一样来使用结构体值的指针。
	book2.pages = book1.pages
}

关于结构体值的比较

大多数的结构体类型都是可比较类型,除了少数含有不可比较字段的结构体类型

和结构体值的赋值规则类似,如果两个结构体值的类型均为可比较类型,则它们仅在它们的类型的底层类型一样(要考虑字段标签)并且其中至少有一个结构体值的类型为非定义类型时才可以互相比较。

如果两个结构体值可以相互比较,则它们的比较结果等同于逐个比较它们的相应字段。 两个结构体值只有在它们的相应字段都相等的情况下才相等。

关于结构体值的类型转换

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

比如,对于下面的代码片段中所示的五个结构体类型:S0S1S2S3S4 特别地, 但是,
package main

type S0 struct {
	y int "foo"
	x bool
}

type S1 = struct { // S1是一个非定义类型
	x int "foo"
	y bool
}

type S2 = struct { // S2也是一个非定义类型
	x int "bar"
	y bool
}

type S3 S2 // S3是一个定义类型。
type S4 S3 // S4是一个定义类型。
// 如果不考虑字段标签,S3(S4)和S1的底层类型一样。
// 如果考虑字段标签,S3(S4)和S1的底层类型不一样。

var v0, v1, v2, v3, v4 = S0{}, S1{}, S2{}, S3{}, S4{}
func f() {
	v1 = S1(v2); v2 = S2(v1)
	v1 = S1(v3); v3 = S3(v1)
	v1 = S1(v4); v4 = S4(v1)
	v2 = v3; v3 = v2 // 这两个转换可以是隐式的
	v2 = v4; v4 = v2 // 这两个转换也可以是隐式的
	v3 = S3(v4); v4 = S4(v3)
}

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

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

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

一个例子:
var aBook = struct {
	author struct { // 此字段的类型为一个匿名结构体类型
		firstName, lastName string
		gender              bool
	}
	title string
	pages int
}{
	author: struct {
		firstName, lastName string
		gender              bool
	}{
		firstName: "Mark",
		lastName: "Twain",
	}, // 此组合字面形式中的类型为一个匿名结构体类型
	title: "The Million Pound Note",
	pages: 96,
}

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

更多关于结构体类型

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

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏