结构体

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

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

struct {
	title  string
	author string
	pages  int
}
var s = struct {
		title  string
		author string
		pages  int
	}{
		title:  "Go",
		author: "yogurt",
		pages:  1024,
	}
	fmt.Println(s)

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

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

struct {
	title, author string
	pages         int
}

一个结构体类型的尺寸为它的所有字段的(类型)尺寸之和加上一些填充字节的数目

常常地,编译器(和运行时)会在一个结构体值的两个相邻字段之间填充一些字节来保证一些字段的地址总是某个整数的倍数

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

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

struct {
	Title  string `json:"title" myfmt:"s1"`
	Author string `json:"author,omitempty" myfmt:"s2"`
	Pages  int    `json:"pages,omitempty" myfmt:"n1"`
	X, Y   bool   `myfmt:"b1"`
}

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

我们可以使用反射来检视字段的标签信息。

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

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

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

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

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

  1. 内存大小不确定: 结构体的大小在编译时必须是固定的。如果一个结构体直接或间接包含其自身类型的字段,那么它的大小将变为无限大,因为每个结构体又包含了另一个同类型的结构体,这样下去是没有结束的。编译器无法为这样的结构体分配内存,因为它不可能确定其大小。
  2. 递归定义无基准: 通常的递归有一个基本情况,让递归有一个结束的点。但如果结构体包含自己,它就没有一个明确的创建实例时的基本情况,这将导致定义上的问题,也就是你无法创建这样的结构体实例。
  3. 可以包含自身指针类型的字段。指针的大小是固定的(通常为 4 或 8 个字节,取决于架构),无论它指向什么。因此,尽管 递归地引用了自己,但编译器可以确定结构体的大小,因为它只是在存储一个指针

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

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

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

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

  • S{0, false}。在此变种形式中,所有的字段名称均不出现,但每个字段的值必须指定,并且每个字段的出现顺序和它们的声明顺序必须一致
  • 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(或v.y)这种形式称为一个选择器(selector)。其中的v称为此选择器的属主。 今后,我们称一个选择器中的句点.属性选择操作符

如果一个组合字面量中最后一项和结尾的}处于同一行,则此项后的逗号,是可选的;否则此逗号不可省略。

关于结构体值的赋值

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

func f() {
	book1 := Book{pages: 300}
	book2 := Book{"Go", "yogurt", 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 := &(book.Pages)   
	*p = 123
	fmt.Println(book) // {123}

	// 下面这两行编译不通过,因为Book{}是不可寻址的,
	// 继而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
}

TODO :可寻址和可取址

https://juejin.cn/post/7077800061417029640

在这个表达式中,Book{100}是一个临时的、未命名的结构体实例,它其实是不可寻址的,因为它不具备一个稳定的地址,但是可以通过&操作符直接对其取地址,并赋予指针变量p。这样做实际上是创建了一个临时变量(就像你注释中的tmp),虽然这个中间变量在源代码中不可见。

这个背后的理念是,即使这个结构体实例在字面上看是直接用在了取地址表达式中,Go语言的编译器将它实质上创建在内存中(像你的注释中tmp变量那样),因此你得到的指针p指向的是一个有效的且可寻址的内存地址。

这种情况通常适用于暂时的值直接用在&操作符的地方——编译器可以优化这种情况,把临时值放在一个可以寻址的位置上,这样它就可以被取地址了。这种行为与你是否可以在其他情境下取得该值的地址(例如,将其分配给一个变量后再取地址)无关。

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

package main

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

关于结构体值的比较

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

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

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

关于结构体值的类型转换

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

比如,对于下面的代码片段中所示的五个结构体类型:S0S1S2S3S4

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

特别地,

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

但是,

  • S2表示的类型的值必须被显式转换为类型S1,反之亦然。
  • 类型S3的值必须被显式转换为类型S4,反之亦然。
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,
}

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