本章节介绍结构体,通过本章节,你将了解到如何初始化结构体、结构体的零值、怎样将自定义的默认值分配给结构体的数据字段、引用结构体的值,还会介绍到值引用和指针引用的区别。

7.1 什么是结构体

结构体是声明了数据类型的数据字段的集合,这些数据类型可以相同或不同,通过结构体的变量名可以访问一系列分组值。结构体丰富了编程过程中创建数据结构的方法,是很常用的基础知识之一。

我们来尝试用结构体记录下库里某场 NBA 比赛的个人数据:

package main

import "fmt"

type PersonalData struct {
    Name string
    Points int
    Assists int
    Rebounds int
}

func main() {
    var Pd PersonalData
    Pd.Name = "Curry"
    Pd.Points = 51
    Pd.Assists = 13
    Pd.Rebounds = 5
    fmt.Printf("%+v\n", Pd)
}

代码释义:

  • var 关键字声明变量 Pd,类型是 PersonalData 结构体类型;

  • 使用 . 表示法,为结构体中的字段赋值;

  • 打印结构体的值到终端;

  • 使用 type 关键字自定义结构体类型 PersonalData。在定义时并未分配值,所以会被分配零值。string 类型,零值为””,int 类型零值为0。

也可以使用 new 关键字创建结构体实例:

package main

import "fmt"

type PersonalData struct {
    Name string
    Points int
    Assists int
    Rebounds int
}

func main() {
    Pd := new(PersonalData)
    Pd.Name = "Curry"
    Pd.Points = 51
    Pd.Assists = 13
    Pd.Rebounds = 5
    fmt.Printf("%+v\n", Pd)
}

当然,还可以使用短变量赋值来创建结构体实例:

package main

import "fmt"

type PersonalData struct {
    Name string
    Points int
    Assists int
    Rebounds int
}

func main() {
    Pd := PersonalData {
        Name: "Curry",
        Points: 51,
        Assists: 13,
        Rebounds: 5,
    }
    fmt.Printf("%+v\n", Pd)
}

使用字段名称创建实例时,可以将值分配给结构体中的字段,之间用冒号分割,没有被赋值的字段,则被赋予零值。也可以省略字段名,但要按照声明时的顺序赋值,出于可维护性考虑,不建议在赋值时省略字段名。

随着字段列表的增加,为了提高可维护性和可读性,每个字段和值可以独立成行,且以逗号结尾。使用短变量赋值创建结构体实例是最常见的方式,也是推荐的方式。

7.2 匿名结构体

在定义结构体时不指定结构体的名称,就是匿名结构体。匿名结构体只能在函数内部定义,用于临时性功能。在定义时,直接给匿名结构体初始化一个变量:

var user struct {
    id int
    name string
}
user.id = 30
user.name = "Curry"

// or

user := struct {
    id int
    name string
}{30, "Curry"}

7.3 内嵌结构体

内嵌结构体又叫结构体的命名组合。有时,数据结构有多层嵌套,比较复杂。这时将一个结构体嵌套在另一个结构体中,可能是对复杂结构建模的有效方法。下面是一个球员列表的示例,需要为每位球员存储一个地址。地址可以作为独立的数据结构,并且能很好的映射到球员列表的结构体中:

package main

import "fmt"

type Address struct {
    Number int
    Street string
    City string
}

type PlayerList struct {
    Name string
    PlayerNumber int
    Address Address
}

func main()  {
    pl := PlayerList{
        Name: "Curry",
        PlayerNumber: 30,
        Address: Address{
            Number: 18,
            Street: "SAN mateo avenue",
            City: "San Francisco",
        },
    }
    fmt.Printf("%+v\n", pl)
}

访问嵌套结构体中的数据,可以使用 . 表示法,通过结构体的变量名称,后跟.,然后是数据元素名,再跟一个.,最后是嵌套数据的元素名称:

fmt.Println(pl.Address.City)

7.4 匿名内嵌结构体

嵌入结构体时,可以省略属性名,类似面向对象中的继承。Go 会以隐藏的方式给它分配一个和类型相同的名字。

package main

import "fmt"

type Address struct {
    Number int
    Street string
    City string
}

type PlayerList struct {
    Name string
    PlayerNumber int
    Address
}

func main()  {
    pl := PlayerList{
        Name: "Curry",
        PlayerNumber: 30,
        Address: Address{
            Number: 18,
            Street: "SAN mateo avenue",
            City: "San Francisco",
        },
    }
    fmt.Printf("%+v\n", pl)
    fmt.Println(pl.City)
    fmt.Println(pl.Address.City)
}

命名嵌入和匿名嵌入的区别:

  1. 命名嵌入:如果访问组合(嵌入的对象)必须使用对象名访问;

  2. 匿名嵌入:可以通过结构体的实例名直接访问。访问对象时,如果本地属性不存在,则在嵌入的匿名对象中查找;

  3. 如果多个结构体中都包含相同的属性名,访问匿名嵌入的结构体属性时必须通过完整的访问关系进行访问。

7.5 自定义结构体默认值

在初始化结构体时未对元素赋值,那么它们的默认值是对应类型的零值。尽管 Go 没有提供原生的创建结构体时分配自定义默认值的方法,但我们可以通过构造函数来实现。例如,PlayerList 结构体,用来记录 NBA 球员的信息,这时可以将 League 字段的值默认设置为 NBA,我们只需要填写球员名字和球衣号码即可:

package main

import "fmt"

type PlayerList struct {
    League string
    Name string
    PlayerNumber int
}

func NewPlayerList(name string, number int) PlayerList {
    pl := PlayerList{
        League: "NBA",
        Name: name,
        PlayerNumber: number,
    }
    return pl
}

func main()  {
    fmt.Printf("%v\n", NewPlayerList("Curry", 30))
}

7.6 结构体比较

对结构体进行比较,可以判断它们是否为同一类型,是否有相同值。==和!=用来判断两边的数据是否相等或不等:

package main

import "fmt"

type NBA struct {
 Name          string
 UniformNumber int
}

func main() {
 Curry := NBA {
  Name:          "Curry",
  UniformNumber: 30,
 }
 Durant := NBA {
  Name:          "Kevin",
  UniformNumber: 15,
 }

 if curry == Durant {
  fmt.Println("The Same.")
 } else {
  fmt.Printf("%v\n", Curry)
  fmt.Printf("%v\n", Durant)
 }
}

不同类型的结构体进行比较会引发编译错误,因此,在做对比前需要检查结构体是否为同一类型。reflect 包提供了这个功能:

package main

import (
 "fmt"
 "reflect"
)

type NBA struct {
 Name          string
 UniformNumber int
}

func main() {
 Curry := NBA{
  Name:          "Curry",
  UniformNumber: 35,
 }
 Durant := NBA{
  Name:          "Kevin",
  UniformNumber: 15,
 }

 fmt.Println(reflect.TypeOf(Curry))
 fmt.Println(reflect.TypeOf(Durant))
}

7.7 公有值和私有值

如果值导出到函数、方法或包外依然可用,则为公有值,否则为私有值。私有值仅在其上下文中可用。结构体和结构体中的元素,都可以根据 Go 约定导出或不导出。以大写字母开头的是可以导出的,其他则不能。为了导出结构体和其元素,结构名和元素属性名必须以大写字母开头。

7.8 值引用和指针引用

使用结构体时,熟悉值引用和指针引用很有必要。在第三章变量章节,介绍了数据的值存储在内存中。指针保存了一个值的存储地址,可以通过引用指针,读取或写入已存储的值。在初始化结构体时,将为结构体中的元素及其默认值分配内存。使用短变量声明变量,并分配默认值:

Curry := NBA{}

如果复制 Curry 结构体,在内存上有一个重要的区别需要理解:将引用该结构体的变量分配给另一个值时,称为值分配:

Durant := Curry

此时 Durant 与 Curry 相同,是 Curry 的副本,而不是对 Curry 的引用。对 Durant 的任何更改,都不会体现在 Curry 上,反之亦然。

package main

import "fmt"

type NBA struct {
    Name          string
    Team string
}

func main() {
    Curry := NBA{
        Name:          "Curry",
        Team: "Warriors",
    }

    Durant := Curry

    fmt.Printf("%v\n", Curry)
    fmt.Printf("%v\n", Durant)

    fmt.Printf("%v\n", &Curry)
 fmt.Printf("%v\n", &Durant)
}

代码释义:

  • 定义一个名称为 NBA 的结构体

  • 定义一个 Curry 变量,并赋值为 NBA

  • 定义一个 Durant 变量,并赋值为 Curry

  • 打印 Curry 和 Durant 的值

  • 打印 Curry 和 Durant 的内存地址

如果要修改原始结构中包含的值,应该使用指针。指针是对内存地址的引用,不会对结构体的副本进行操作,而是会更新引用基础内存的所有变量。下面是更新指针值的示例:

package main

import "fmt"

type NBA struct {
    Name          string
    UniformNumber int
}

func main() {
    Curry := NBA{
        Name:          "Curry",
        UniformNumber: 35,
    }
    Durant := &Curry
    Durant.UniformNumber = 15

    fmt.Printf("%v\n", Curry)
    fmt.Printf("%v\n", *Durant)

    fmt.Printf("%v\n", &Curry)
    fmt.Printf("%v\n", Durant)
}

变动部分的代码释义:

  • Durant 不再是通过值引用,而是通过指针引用

  • 当 Durant 更新时,分配给 Curry 的底层内存也更新了。由于 Curry 和 Durant 都引用了相同的底层内存,所以它们的值是相同的

  • 打印 Durant 和 Curry 的值可以看到结果相同。需要注意的是,Durant 现在是一个指针,必须使用*取消对它的引用

  • 打印 Durant 和 Curry 的底层内存地址,结果也是相同的

指针引用和值引用之间的区别很细微,但是选择使用指针还是值很简单。如果需要修改(或改变)结构体的初始化值,请使用指针引用。如果需要对结构体进行操作,但不希望修改结构体的初始化值时,请使用值引用。

7.9 总结

本章主要介绍结构体(Struct),包括如何创建结构体、如何使用结构体表示复杂的数据结构。在对结构体进行初始化时,如果使用顺序初始化的方式,每个成员必须进行初始化;如果使用指定成员初始化,没有初始化的成员,自动赋零值。使用构造函数在创建结构体时自定义值。了解了结构体的导出及其指针引用与值引用的区别。结构体提供了一种以编程方式进行推理的方法,是一种强大的基本结构。当你再观察身边事物的时候,可以尝试在结构体中表示它们,比如一副口罩、一只鸟。

文档更新时间: 2021-11-08 10:07   作者:admin