有时需要保存超过一种类型的数据。
切片可以保存一组数据,映射能保存一组键和一组值,这两种数据结构都只能保存一种类型。有时需要一组不同类型的数据,例如邮件地址,其中包含了街道名、邮编等,无法使用切片或者映射来保存,但是可以使用 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增加新字段会使代码更干净。