Go 结构体

admin
admin 2020年03月17日
  • 在其它设备中阅读本文章

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

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

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

struct {
    title  string
    author string
    pages  int
}

上面这个结构体类型含有三个字段。前两个字段(title 和 author)的类型均为 string。最后一个字段 pages 的类型为 int。有时字段也称为成员变量。相邻的同类型字段可以声明在一起。比如上面这个类型也可表示成下面这样:

struct {
    title, author string
    pages         int
}

每个结构体字段在它的声明中可以被指定一个 标签(tag)。字段标签可以是任意字符串,它们是可选的,默认为空字符串。下面这个结构体类型中的字段均被指定了标签。Go 标准库中的反射包可以获取字段标签

struct {
    Title  string `json:"title"`
    Author string `json:"author,omitempty"`
    Pages  int    `json:"pages,omitempty"`
}

一个结构体类型中的字段标签和字段的声明顺序对此结构体类型的身份识别很重要。如果两个结构体类型的各个对应字段声明都相同(按照它们的出现顺序),则此两个结构体类型是等同的。两个字段声明只有在它们的名称、类型和标签都等同的情况下才相同。一个结构体类型不能(直接或者间接)含有一个类型为此结构类型的字段。

结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。结构体的定义格式如下:

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}

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

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

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
}

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

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

三、结构体的相关操作

1、结构体值的赋值

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

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
}

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

2、结构体字段的可寻址性

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

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
}

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

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

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
}

4、结构体值的比较

大多数的结构体类型都是可比较类型,除了少数含有 不可比较字段 的结构体类型。和结构体值的赋值规则类似,如果两个结构体值的类型均为可比较类型,则它们仅在它们的类型的底层类型一样(要考虑字段标签)并且其中至少有一个结构体值的类型为非定义类型时才可以互相比较。
如果两个结构体值可以相互比较,则它们的比较结果等同于逐个比较它们的相应字段。两个结构体值只有在它们的相应字段都相等的情况下才相等。

5、结构体值的类型转换

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

类型 S0 的值不能被转换为其它四个类型中的任意一个,原因是它与另外四个类型的对应字段名不同(因此底层类型不同)。
类型 S1S2S3S4的任意两个值可以转换为对方的类型。特别地,
类型 S2 的值可以被隐式转化为类型 S3,反之亦然。
类型 S2 的值可以被隐式转换为类型 S4,反之亦然。但是,
类型 S1 的值必须被显式转换为类型 S2,反之亦然。
类型 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)
}

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

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

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

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,
}

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

7、结构体匿名(或内嵌)字段

结构体可以包含一个或多个 匿名 (或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时 类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

type innerS struct {
    in1 int
    in2 int
}

type outerS struct {
    b int
    c float32
    int      // 匿名字段
    innerS   // 匿名字段
}

Go 语言的结构体内嵌有如下特性。
1) 内嵌的结构体可以直接访问其成员变量
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c
2) 内嵌结构体的字段名是它的类型名
内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名,所以一个结构体只能嵌入一个同类型的成员。