8.结构体:struct

Saturday, July 29, 2023

有时需要保存超过一种类型的数据。

切片可以保存一组数据,映射能保存一组键和一组值,这两种数据结构都只能保存一种类型。有时需要一组不同类型的数据,例如邮件地址,其中包含了街道名、邮编等,无法使用切片或者映射来保存,但是可以使用 struct 类型来保存。

struct是由多种类型的值构成

使用struct关键字来声明一个 struct 类型,后面跟花括号,在括号中,可以定义一个或多个字段:struct 组合的值。每一个字段定义在一个单独的行,由字段名称、后面跟着的字段需要保存的值类型组成。

可以使用一个struct类型来作为定义的变量的类型,例:

var myStruct struct {
       number float64
       word   string
       toggle bool
}
fmt.Printf("%#v\n", myStruct)

输出:

struct { number float64; word string; toggle bool }
{number:0, word:"", toggle:false}   //struct中每个字段都被设置成其类型的零值

当调用Printf中的 %#v 时,将myStruct中的值作为struct字面量打印。

写struct字段时,仅仅在字段名称和类型之间插入一个空格即可,go fmt 会自动格式化。

使用点运算符访问struct字段

我们使用点运算符来表示函数属于另一个包,或方法属于一个值。

与此类似,可以使用点运算符来标识属于struct的字段,这也可以用于它们的赋值和检索。

var myStruct struct {
        number float64
        word   string
        toggle bool
}
myStruct.number = 3.14
myStruct.word = "pie"
myStruct.toggle = true   //给struct字段赋值
fmt.Println(myStruct.number)  
fmt.Println(myStruct.word)
fmt.Println(myStruct.toggle)  //从struct字段取值

在struct中保存订阅者的数据

我们知道了如何声明一个保存了struct的变量和如何给字段赋值,可以创建一个来保存杂志订阅者的数据。

var subscriber struct {
        name   string
        rate   float64
        active bool
}
subscriber.name = "Aman Singh"
subscriber.rate = 4.99
subscriber.active = true
fmt.Println("Name:", subscriber.name)
fmt.Println("Monthly rate:", subscriber.rate)
fmt.Println("Active?", subscriber.active)

尽管订阅者的数据使用了多种方式存储,struct让我们把这些都集中在一个方便的包里。

定义类型和struct

定义struct变量很麻烦,定义每一个变量时需要重复整个struct类型声明。

var subscriber1 struct {
        name   string
        rate   float64
        active bool
}      //为subscriber1变量定义struct类型
subscriber1.name = "Aman Singh"
fmt.Println("Name:", subscriber1.name)
var subscriber2 struct {
        name   string
        rate   float64
        active bool
}     //为subscriber2变量定义struct类型
subscriber2.name = "Beth Ryan"
fmt.Println("Name:", subscriber2.name)

我们已经使用了多种类型,像int、string、bool、切片、映射和struct,但是无法创建全新类型。

类型定义允许自己创建新的类型,可以基于基础类型来创建新的定义类型。

定义一个类型,需要使用type关键字,后跟新类型的名字,然后是希望基于的基础类型,若使用struct类型作为基础类型,需要使用struct关键字,后跟以花括号包裹的一组字段定义,就像声明struct变量时一样。

与变量一样,类型定义可以被放在一个函数中,但把它的作用域限定在该函数块中,意味着不能在函数外面使用,所以类型经常定义在函数外的包级别。

package main

import "fmt"

type part struct {    //定义一个名为part的类型,part基础类型有下面这俩字段的结构体
	description string
	count       int
}   

type car struct {    //定义一个名为car的类型,car的基础类型是有下面这些字段的struct
	name     string
	topSpeed float64
}

func main() {
	var porsche car    //定义一个car类型的变量
	porsche.name = "Porsche 911 R"   //访问struct的字段
	porsche.topSpeed = 323
	fmt.Println("Name:", porsche.name)
	fmt.Println("Top speed:", porsche.topSpeed)

	var bolts part
	bolts.description = "Hex bolts"
	bolts.count = 24
	fmt.Println("Description:", bolts.description)
	fmt.Println("Count:", bolts.count)
}

当这些变量被声明后,可以设置这些字段的值和取回这些值。

定义一个类型

package main

import "fmt"

type subscriber struct {
        name    string
        rate    float64
        active  bool
}

func main() {
        var sub subscriber   //定义一个subscriber类型的变量
        sub.name = "Aman Singh"
        fmt.Println("Name:", sub.name)
        var sub2 subscriber  //使用subscriber类型定义第二个变量
        sub2.name = "Beth Ryan"
        fmt.Println("Name:", sub2.name)
        
}

与函数一起使用已定义类型

已定义类型可以用于变量类型以外的地方,也可以用于函数参数和返回值。

package main

import "fmt"

type part struct {
	description string
	count       int
}

func showInfo(p part) { //声明一个以part作为类型的参数
	fmt.Println("Description:", p.description)
	fmt.Println("Count:", p.count)
}

func main() {
	var bolts part
	bolts.description = "Hex bolts"
	bolts.count = 24
	showInfo(bolts) //将part传递给函数
}

函数接受一个单独的参数,使用part作为参数的类型,在showInfo内部,可以像其他struct变量一样通过参数变量访问字段。

还有一个minimumOrder函数来根据特定的描述创建part和预赋值其中的count字段,将minimumOrder的返回值类型定义为part,这样就能返回一个新的struct:

func minimumOrder(description string) part {  //声明一个part类型的返回值
        var p part    //创建一个part值
        p.description = description
        p.count = 100
        return p   //返回part
}

func main() {
        p := minimumOrder("Hex bolts")     //调用minimumOrder,使用一个变量的短声明来保存返回的part
        fmt.Println(p.description, p.count)
}

下面例子,printInfo函数接受subscriber作为参数,并输出它的字段值,defaultSubscriber函数来建立一个新的subscriber struct并赋一些初始的值,它们接受一个名为name的字符串参数并用它来设置新的subscriber的name字段,然后把rate和active字段设置为默认值,最后将整个subscriber struce返回给调用者。

package main

import "fmt"

type subscriber struct {
	name   string
	rate   float64
	active bool
}

func printInfo(s subscriber) { //声明一个参数,使用subscriber类型
	fmt.Println("Name:", s.name)
	fmt.Println("Monthly rate:", s.rate)
	fmt.Println("Active:", s.active)
}

func defaultSubscriber(name string) subscriber { //返回一个subscriber值
	var s subscriber //创建一个新的subscriber
	s.name = name
	s.rate = 5.99
	s.active = true //设置struct字段
	return s        //返回subscriber
}

func main() {
	subscriber1 := defaultSubscriber("Aman Singh") //使用名字来建立一个subscriber
	subscriber1.rate = 4.99                        //使用一个特定的费率
	printInfo(subscriber1)                         //打印字段值
	subscriber2 := defaultSubscriber("Beth Ryan")  //使用名字来建立一个subscriber
	printInfo(subscriber2)                         //打印字段值
}

输出:

Name: Aman Singh
Monthly rate: 4.99
Active: true
Name: Beth Ryan
Monthly rate: 5.99
Active: true

在主函数中,可以将subscriber的名称传递给defaultSubscriber来创建一个新的subscriber struct,一个subscriber获取一个打折的费率,所以直接重设struct的字段,传递给一个已经填充完全的subscriber struct给printInfo函数来打印它的内容。

不要使用一个已经存在的类型名称作为变量的名称!

使用函数修改struct

go 是一个按值传递的语言,函数调用时接收的是一个参数的拷贝,若函数修改了参数值,其修改的只是拷贝,而不是原始值。

对于 struct 也是一样。

函数能够接收指针来代替直接接收值,当调用这个函数时,使用取址运算符&来传送需要更新的值的指针,然后在函数内部,使用*来更新指针指向的值。

func main() {
        amount := 6
        double(&amount)   //传递一个指针来取代变量的值
        fmt.Println(amount)
}
func double(number *int) {  //接受一个指针来代替int值
        *number *= 2  //使用指针更新值
}

输出:

12

使用指针来让函数也能更新struct:更新 s 参数类型来接受一个 subscriber struct 的指针,而不是直接使用struct,然后更新 struct 中的rate字段,在main中,调用applyDiscount并传入 subscriber struct 的指针,在输出rate字段时,能看到被更新成功。

package main

import "fmt"

type subscriber struct {
	name   string
	rate   float64
	active bool
}

func applyDiscount(s *subscriber) { //获取struct的指针,而不是struct
	s.rate = 4.99 //更新struct字段
}

func main() {
	var s subscriber
	applyDiscount(&s) //传入一个指针,而不是struct
	fmt.Println(s.rate)
}

在double函数中,需要使用 * 运算符来获取指针指向的值,当在applyDiscount函数中设置rate字段时不需要 * 吗?

使用点运算符在struct指针和struct上都可以访问字段。

//自动解引用的写法
s.rate = 4.99

//显式解引用的写法
(*s).rate = 4.99

golang中以下场景会自动解引用:

  • 结构体指针访问字段。
  • 指针类型的接收者方法调用。
  • 指针类型的数组或切片访问元素。
  • 值类型参数的函数调用中传递指针。

通过指针访问struct的字段

若尝试打印一个变量的指针,会看到其指向的内存地址:

func main() {
        var value int = 2
        var pointer *int = &value
        fmt.Println(pointer)  //输出的是指针不是值
}

应该使用 * 运算符,就像调用取值运算符一样,来获取指针指向的值。

func main() {
        var value int = 2
        var pointer *int = &value
        fmt.Println(*pointer)  //输出指针指向的值
}

所以也需要对指向 struct 的指针使用 * 运算符,但是直接把 * 放到指针前是无法工作的,需要使用括号包裹。

type myStruct struct {
        myField int
}

func main() {
        var value myStruct
        value.myField = 3
        var pointer *myStruct = &value
        fmt.Println((*pointer).myField)   //获取指针指向的的struct值,然后访问struct的字段
}

点运算符允许通过struct的指针来访问字段,就像可以通过struct值直接访问一样,可以不需要括号和*运算符。

func main() {
        var value myStruct
        value.myField = 3
        var pointer *myStruct = &value
        fmt.Println(pointer.myField)   //通过指针访问struct字段
}

也可以通过指针来赋值给struct字段:

func main() {
        var vaule myStruct
        var pointer *myStruct = &value
        pointer.myField = 9     //通过指针来赋值给struct字段
        fmt.Println(pointer.myField)
}

这就是上边 applyDiscount 函数可以更新struct字段而不用*运算符,它通过struct指针赋值给rate字段。

func applyDiscount(s *subscriber) { //获取struct的指针,而不是struct
	s.rate = 4.99 //通过指针赋值给struct字段
}

func main() {
	var s subscriber
	applyDiscount(&s) //传入一个指针,而不是struct
	fmt.Println(s.rate)
}

在设置struct字段前展示了defaultSubscriber函数,但是它不需要任何指针,这是为什么?
答:defaultSubscriber函数返回了一个struct值,若调用者保存了返回的值,那么struct值中的字段同时会被保存。只有函数修改已经存在的struct而没有返回它们的时候需要使用指针来保存修改项。
若需要时,defaultSubscriber可以返回一个struct的指针。

使用指针传递大型struct

函数形参接收一个函数调用的实参的拷贝,即使 struct 也是如此,如果传递一个有很多字段的大的 struct,会占用很多电脑内存,它会为原始的 struct 和被拷贝的 struct 都划分空间。

函数行参接收一个它们被调用时的参数的拷贝,即使是struct也是这样。

除非struct只有一些小字段,否则向函数传入struct的指针是一个好主意,即使函数并不想修改struct也是如此。当传递一个struct指针时,内存中只有一个原始的struct,并可以读取、修改等操作,都不会产生一个额外的拷贝。

package main

import "fmt"

type subscriber struct {
	name   string
	rate   float64
	active bool
}

func printInfo(s *subscriber) { //获取指针
	fmt.Println("Name:", s.name)
	fmt.Println("Monthly rate:", s.rate)
	fmt.Println("Active:", s.active)
}

func defaultSubscriber(name string) *subscriber { //返回指针
	var s subscriber
	s.name = name
	s.rate = 5.99
	s.active = true
	return &s //返回一个指向struct的指针,而不是struct自己
}

func applyDiscount(s *subscriber) {
	s.rate = 4.99
}

func main() {
	subscriber1 := defaultSubscriber("Aman Singh") //一个struct指针
	applyDiscount(subscriber1)                     //由于这里已经是struct的指针,去掉取址运算符
	printInfo(subscriber1)
	subscriber2 := defaultSubscriber("Beth Ryan")
	printInfo(subscriber2)
}

这里defaultSubscriber函数,更改为返回一个指针,且printInfo函数也改成接受一个指针,这些函数不需要像applyDiscount一样修改struct,使用指针确保对于每个struct值,只有一个拷贝在内存中,也能保证程序正常工作。

将struct类型移动到另一个包

将type类型保存在magazine包中:

package magazine

type Subscriber struct {
        name   string
        Rate   float64
        active bool
}
package main

import (
       "fmt"
       "xxx/xxx/magazine"
)

func main() {
        var s magazine.Subscriber
        s.Rate = 4.99
        fmt.Println(s.Rate)
}

Go类型名称与变量和函数名称规则一样,首字母大写被认为可导出,可以从外部包来访问。

从包中导出struct字段,字段名称的首字母也必须大写。

struct字面量

代码定义一个struct并且一个一个的赋值,很不友好,跟切片、映射一样,Go提供了struct字面量来让你创建一个struct并同时为其字段赋值。

类型列在前,跟着一对花括号,在花括号内部,可以给一些或所有的struct字段赋值,使用字段名称、冒号和值,若定义多个字段,使用逗号分隔。

使用struct字面量,创建Subscriber并设置值:

subscriber := magazine.Subscriber{Name: "Aman Singh", Rate: 4.99, Active: true}
fmt.Println("Name:", subscriber.Name)
fmt.Println("Rate:", subscriber.Rate)
fmt.Println("Active:", subscriber.Active)

不得不对struct变量使用长声明的方式(除非struct从一个函数中返回),struct字面量允许对刚创建的struct使用短变量声明。

可以在花括号中忽略一些甚至所有的字段,被忽略的字段会被设置为它们的零值。

subscriber := magazine.Subscriber{Rate: 4.99}
fmt.Println("Name:", subscriber.Name)
fmt.Println("Rate:", subscriber.Rate)
fmt.Println("Active:", subscriber.Active)

输出:

Name:           //被忽略的字段获得了零值
Rate: 4.99
Active: false   //被忽略的字段获得了零值

创建一个Employee struct类型

这个新的magazine包,在发布前,需要一个Employee strcut类型来追踪营业员的名称和薪水,并需要把邮件地址放到雇员和订阅者的结构中。

在magazine包中增加一个类型:

pacakge magazine

type Subscriber struct {
        Name    string
        Rate    float64
        Active  bool
}

type Employee struct {
        Name    string
        Salary  float64
}
package main

import (
        "fmt"
        "xxx/xxx/magazine"
)

func main() {
        var emplpoyee magazine.Employee
        emplpoyee.Name = "Joy Carr"
        emplpoyee.Salary = 60000
        fmt.Println(emplpoyee.Name)
        fmt.Println(emplpoyee.Salary)
}

创建一个Address struct类型

在Subscriber和Employee类型中增加一个地址,街道、城市、州、和邮编。

type Subscriber struct {
        Name    string
        Rate    float64
        Active  bool
        Street  string
        City    string
        State   string
        PostalCode string
}

type Employee struct {
        Name    string
        Salary  float64
        Street  string
        City    string
        State   string
        PostalCode string
}

但是邮政地址都有相同的格式,无论属于哪个类型,将所有字段在多个类型中重复是痛苦的事情。

struct字段可以保存任何类型,甚至是struct类型,所以我们创建一个Address struct 类型,然后在Subscriber和Employee类型中增加一个Address类型的字段,并且如果想要修改地址格式,可以保证类型之间的一致性。

type Address struct {
        Street  string
        City    string
        State   string
        PostalCode string
}
package main

import (
        "fmt"
        "xxx/xxx/magazine"
)

func main() {
        var address magazine.Address
        address.Street = "123 Oak St"
        address.City = "Omaha"
        address.State = "NE"
        address.PostalCode = "12345"
        fmt.Println(address)
}

将struct作为字段增加到另一个类型中

给 Subscriber 和 Employee 类型增加一个HomeAddress字段,在struct类型中增加一个struct类型字段与增加其他类型字段相同,字段名称和字段类型。

package magazine

type Address struct {
        Street  string
        City    string
        State   string
        PostalCode string
}

type Subscriber Struct {
        Name    string
        Rate    float64
        Active  bool
        HomeAddress Address
}

type Employee struct {
        Name    string
        Salary  float64
        HomeAddress Address
}

在另一个struct中设置struct

在Subscriber struct中设置 Address struct的字段值,这里有两种方式。

第一种是创建一个独立的Address struct并使用它填充 Subscriber的整个Address字段:

package main

import (
        "fmt"
        "xxx/xxx/magazine"
)

func main() {
        address := magazine.Address{Street: "123 Oak St", City: "Omaha", State: "NE", PostalCode: "12345"}
        subscriber := magazine.Subscriber{Name: "Aman Singh"}  //创建一个Address所属的Subscriber struct
        subscriber.HomeAddress = address    //设置HomeAddress字段
        fmt.Println(subscriber.HomeAddress)  //打印HomeAddress字段
}

另一种方法是通过外部struct来设置内部struct的字段。
当一个Subscriber struct被创建后,其HomeAddress字段也被设置:它是一个Address struct,所有的字段都被设置为零值,若对fmt.Printf使用"%#v"动词来打印HomeAddress,会打印出它在go代码中的样子,也就是说像struct字面量一样,将看到每一个Address字段被设置为空字符串,也就是string类型的零值。

subscriber := magazine.Subscriber()
fmt.Printf("%#v\n", subscriber.HomeAddress)

输出:

magazine.Address{Street:"", City:"", State:"", PostalCode:""}  //字段已经像Address struct一样被设置了,每一个字段都被设置为空字符串(字符串的零值)

若subscriber是一个包含Subscriber struct的变量,当输入subscriber.HomeAddress时,会获得一个Address struct,即使没有明确设置HomeAddress。

可以使用点运算符“链”的方式来访问Address struct中的字段,简单使用subscriber.HomeAddress来访问Address struct,后面跟另一个点运算符和想要访问的Address struct中的字段名称:

subscriber.HomeAddress.City    //subscriber.HomeAddress 这部分返回一个Address struct,City 部分访问Address struct中的City部分

在subscriber变量中保存一个Subscriber struct,在subscriber的HomeAddress字段中自动创建一个Address struct,设置subscriber.HomeAddress.Street、subscriber.HomeAddress.City和其他的值,然后输出这些值。然后在employee变量中保存Employee struct,并在HomeAddress struct中做相同操作。

package main

import (
	"fmt"
	"xxx/xxx/magazine"
)

func main() {
	subscriber := magazine.Subscriber{Name: "Aman Singh"}
	subscriber.HomeAddress.Street = "123 Oak St"
	subscriber.HomeAddress.City = "Omaha"
	subscriber.HomeAddress.State = "NE"
	subscriber.HomeAddress.PostalCode = "12345"
	fmt.Println("Subscriber Name:", subscriber.Name)
	fmt.Println("Street:", subscriber.HomeAddress.Street)
	fmt.Println("City:", subscriber.HomeAddress.City)
	fmt.Println("State", subscriber.HomeAddress.State)
	fmt.Println("Postal Code:", subscriber.HomeAddress.PostalCode)

	// ...  //employee 省略
}

匿名struct字段

通过外部struct访问内部struct的字段有点繁琐,想要访问内部struct字段的时候,不得不每次输入代表struct字段的名称。go允许定义一个匿名字段:struct字段没有名字,仅仅只有类型,可以使用匿名字段来让内部struct访问更简单。

更新 Subscriber 和 Employee 类型,让HomeAddress字段作为一个匿名字段,只需移除字段名称,仅保留字段类型。

package magazine

type Subscriber struct {
        Name string
        Rate float64
        Active bool
        Address
}

type Employee struct {
        Name  string
        Salary float64
        Address   //删除字段名称(HomeAddress),仅保留类型
}

type Address struct {
        // Fields omitted
}

当声明一个匿名字段时,可以使用字段类型名称作为字段名称,所以 subscriber.Address和 employee.Address在下面的代码中仍然访问Address struct:

subscriber := magazine.Subscriber{Name: "Aman Singh"}
subscriber.Address.Street = "123 Oak St"
subscriber.Address.City = "Omaha"
fmt.Println("Street:", subscriber.Address.Street)
fmt.Println("City:", subscriber.Address.City)
//...

嵌入struct

匿名字段不只是使struct定义中省略了字段名称。

一个内部struct使用匿名字段的方式存储在了外部的struct中,这被称为嵌入了外部struct,嵌入struct的字段被提升到了外部struct,可以像访问外部struct的字段一样访问它们。

所以Address struct类型被嵌入了Subscriber struct和Employee struct类型,不需要写下subscriber.Address.City来获取City字段,可以只写subscriber.City,不需要写下 employee.Address.State,可以只写employee.State。

修改为将Address当作一个内嵌类型,可以将代码写成完全没有Address类型,就像Address的字段属于它嵌入的struct类型。

package main

import (
	"fmt"
	"xxx/xxx/magazine"
)

func main() {
	subscriber := magazine.Subscriber{Name: "Aman Singh"}
	subscriber.Street = "123 Oak St"
	subscriber.City = "Omaha"
	subscriber.State = "NE"
	subscriber.PostalCode = "12345"  //设置Address的字段就像它们在Subscriber上被定义过一样
	fmt.Println("Subscriber Name:", subscriber.Name)
	fmt.Println("Street:", subscriber.Street)
	fmt.Println("City:", subscriber.City)
	fmt.Println("State", subscriber.State)
	fmt.Println("Postal Code:", subscriber.PostalCode)

	...  //employee 省略

}

不是必须内嵌内部struct,也不是必须使用内部struct,有时给外部struct增加新字段会使代码更干净。

Golang打怪升级

9.定义类型

7.数据标签:映射

comments powered by Disqus