结构体
1 什么是结构体?
结构体是用户定义的类型,表示若干个字段(Field)的集合,也就是一系列属性的集合,类似于python中的类,但是只有属性没有方法。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。
例如,一个职员有 firstName
、lastName
和 age
三个属性,而把这些属性组合在一个结构体 employee
中就很合理。
2 结构体的声明
type Employee struct {
firstName string
lastName string
age int
}
在上面的代码片段里,声明了一个结构体类型 Employee
,它有 firstName
、lastName
和 age
三个字段。通过把相同类型的字段声明在同一行,结构体可以变得更加紧凑。在上面的结构体中,firstName
和 lastName
属于相同的 string
类型,于是这个结构体可以重写为:
type Employee struct {
firstName, lastName string
age, salary int
}
上面的结构体 Employee
称为 命名的结构体(Named Structure)。我们创建了名为 Employee
的新类型,而它可以用于创建 Employee
类型的结构体变量。
声明结构体时也可以不用声明一个新类型,这样的结构体类型称为 匿名结构体(Anonymous Structure)。
var employee struct {
firstName, lastName string
age int
}
上述代码片段创建一个匿名结构体 employee
。
3 创建命名的结构体
通过下面代码,我们定义了一个命名的结构体 Employee ,它是一种新的类型。
3.1 只定义不赋值
package main
import "fmt"
type Employee struct {
firstName, LastName string
age, salary int
}
func main() {
var emp Employee //结构体的使用,相当于实例化得到一个对象
fmt.Println(emp) //结构体是一个值类型, 默认赋为零值,因此输出为{ 0 0} 两个空字符串,两个0
}
3.2 定义并赋初值
package main
import "fmt"
type Employee struct {
firstName, LastName string
age, salary int
}
func main() {
var emp Employee = Employee{age: 18, LastName: "Anderson"} //关键字指名道姓赋值,不按位置,可以不全部传值
var emp2 Employee = Employee{"Sam", "Anderson", 18, 3000} //按位置赋值,必须全部传值
fmt.Println(emp)
fmt.Println(emp2)
}
上面程序输出为:
{ Anderson 18 0}
{Sam Anderson 18 3000}
我们也可以用简略声明的方法,定义并赋初值:
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
//creating structure using field names 关键字赋值
emp1 := Employee{
firstName: "Sam",
age: 25,
salary: 500,
lastName: "Anderson",
}
//creating structure without using field names 位置赋值
emp2 := Employee{"Thomas", "Paul", 29, 800}
fmt.Println("Employee 1", emp1)
fmt.Println("Employee 2", emp2)
}
在上述程序的第 7 行,我们创建了一个命名的结构体 Employee
。而在第 15 行,通过指定每个字段名的值,我们定义了结构体变量 emp1
。字段名的顺序不一定要与声明结构体类型时的顺序相同。在这里,我们改变了 lastName
的位置,将其移到了末尾。这样做也不会有任何的问题。
在上面程序的第 23 行,定义 emp2
时我们省略了字段名。在这种情况下,就需要保证字段名的顺序与声明结构体时的顺序相同。该程序将输出:
Employee 1 {Sam Anderson 25 500}
Employee 2 {Thomas Paul 29 800}
4 创建匿名结构体
匿名结构体一般用来定义在内部(函数,结构体),只使用一次,没有名字(不声明结构体类型)。
有什么用?当定义多个变量(想一次使用),就可以把这多个变量放到匿名结构体中。下面代码段演示了如何定义匿名结构体:
//1、定义一个有名结构体
type Hobby struct{
HobbyId int64
HobbyName string
}
//2、去掉名字,去掉type关键字,相当于写了一个函数,会报错
struct{
HobbyId int64
HobbyName string
}
//3、给它实例化一下,然后赋值给一个变量,匿名结构体没有类型,因此不能用 var a 没有类型 = struct{}的方式定义
a:= struct{
HobbyId int64
HobbyName string
}{}
定义并初始化一个匿名结构体
package main
import (
"fmt"
)
func main() {
emp := struct {
firstName, lastName string
age, salary int
}{
firstName: "Andreah",
lastName: "Nikola",
age: 31,
salary: 5000,
}
fmt.Println(emp)
fmt.Println(emp.age)
emp.salary = 8000
fmt.Println(emp.salary)
}
上面我们已经提到,之所以称这种结构体是匿名的,是因为它只是创建一个新的结构体变量 emp
,而没有定义任何结构体类型。
该程序会输出:
{Andreah Nikola 31 5000}
31
8000
5 结构体的零值(Zero Value)
结构体是值类型,当定义好的结构体并没有被显式地初始化时,该结构体的字段将默认赋为零值。
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp Employee //zero valued structure
fmt.Println("Employee", emp)
}
该程序定义了 emp
,却没有初始化任何值。因此 firstName
和 lastName
赋值为 string 的零值(""
)。而 age
和 salary
赋值为 int 的零值(0)。该程序会输出:Employee { 0 0}
6 访问结构体的字段
点号操作符 .
用于访问结构体的字段。
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
emp := Employee{"Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", emp.firstName)
fmt.Println("Last Name:", emp.lastName)
fmt.Println("Age:", emp.age)
fmt.Printf("Salary: $%d", emp.salary)
}
上面程序中的 emp.firstName 访问了结构体 emp
的字段 firstName
。该程序输出:
First Name: Sam
Last Name: Anderson
Age: 55
Salary: $6000
还可以创建零值的 struct
,以后再给各个字段赋值。
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp Employee
emp.firstName = "Jack"
emp.lastName = "Adams"
fmt.Println("Employee :", emp)
}
在上面程序中,我们定义了 emp
,接着给 firstName
和 lastName
赋值。该程序会输出:
Employee : {Jack Adams 0 0}
7 结构体的指针
我们还可以创建指向结构体的指针
7.1 只定义不赋值
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp *Employee
fmt.Println(emp)
fmt.Printf("addr of Employee is %p\n", emp)
fmt.Printf("addr of emp is %p\n", &emp)
}
emp是指针,指向结构体Employee,指针是引用类型,默认零值为nil,指向一个空地址,上面程序输出为:
<nil>
addr of Employee is 0x0
addr of emp is 0xc000006028
7.2 定义并初始化指针
package main
import (
"fmt"
)
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp *Employee = &Employee{} //定义指向Employee的指针并初始化
fmt.Println(emp)
fmt.Printf("addr of Employee is %p\n", emp)
fmt.Printf("addr of emp is %p\n", &emp)
}
初始化后,emp指针就不为空了,它存储了Employee的地址,上面程序输出为:
&{ 0 0} //变量emp的值,即指向结构体的地址
addr of Employee is 0xc000076330 //Employee结构体的地址
addr of emp is 0xc000006028 //变量emp自己的地址
指针解引用,通过结构体指针访问并改变结构体的字段
package main
import "fmt"
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp *Employee = &Employee{}
(*emp).firstName = "Tom" //解引用语法
(*emp).lastName = "Cat"
//emp.firstName = "Tom" Go支持直接使用代替显示的解引用(数组也可以这样,语言层面自动帮你处理了)
fmt.Println(emp)
上面程序输出为:&{Tom Cat 0 0}
8 匿名字段
当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。匿名字段的作用?结合嵌套结构体,实现变量提升/提升字段,类似python中面向对象的继承。
以下代码创建一个 Person
结构体,它含有两个匿名字段 string
和 int
。
type Person struct {
string
int
sex string //前两个字段没有名字,会以类型当做它字段的名字,因此第三个字段为string类型的话需要定义名字,否则名字重复了
}
我们接下来使用匿名字段来编写一个程序。
package main
import (
"fmt"
)
type Person struct {
string
int
sex string
}
func main() {
p := Person{"Naveen", 50, "男"} //按位置传值
//p := Person{string:"Naveen",int:50, sex:"男"} //指名道姓传值,字段匿名,类型就是字段名
fmt.Println(p)
fmt.Println(p.string) //取值也是按类型
fmt.Println(p.int)
}
上面的程序输出为:
{Naveen 50 男}
Naveen
50
9 嵌套结构体(Nested Structs)
结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体(结构体中套结构体)。
package main
import (
"fmt"
)
type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address //字段名address 字段类型是结构体Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address {
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:",p.age)
fmt.Println("City:",p.address.city)
fmt.Println("State:",p.address.state)
}
上面的结构体 Person
有一个字段 address
,而 address
也是结构体。该程序输出:
Name: Naveen
Age: 50
City: Chicago
State: Illinois
嵌套结构体的传值跟结构体一样,可以按位置传或者按关键字传值:
package main
import (
"fmt"
)
type Address struct {
city, country string
}
type Person struct {
name string
age int
address Address
}
func main() {
//var p Person = Person{"lqz", 18, Address{"cq", "china"}} 嵌套的结构体按位置传值
var p Person = Person{name: "lqz", age: 18, address: Address{city: "cq", country: "china"}} //嵌套的结构体按指名道姓传值
fmt.Println(p.name)
fmt.Println(p.address.city)
}
上面的程序输出为:
lqz
cq
10 提升字段(Promoted Fields)
如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。我知道这种定义很复杂,所以我们直接研究下代码来理解吧。
type Person struct {
Name string
Age int
Hobby //这是一个匿名字段,没有名字,只有类型,类型正好为结构体类型,属于嵌套结构体
}
type Hobby struct {
HobbyId int
HobbyName string
}
在上面的代码片段中,Person
结构体有一个匿名字段 Hobby
,而 Hobby
是一个结构体。现在结构体 Hobby
有 HobbyId
和 HobbyName
两个字段,访问这两个字段就像在 Person
里直接声明的一样,因此我们称之为提升字段。
package main
import "fmt"
type Person struct {
Name string
Age int
Hobby //这是一个匿名字段,没有名字,只有类型,类型正好为结构体类型,属于嵌套结构体
}
type Hobby struct {
HobbyId int
HobbyName string
}
func main() {
var p Person = Person{Name: "lqz", Age: 18, Hobby: Hobby{HobbyId: 1, HobbyName: "足球"}} //指名道姓传值
fmt.Println(p.HobbyName) //Hobby是一个匿名字段,会字段提升,把Hobby字段内的,HobbyName字段提升到跟Name和Age字段一层了
fmt.Println(p.Hobby.HobbyName)
}
p.HobbyName
访问提升字段,就像它是在结构体 p
中声明的一样,因此上面程序输出的都是足球
。
字段提升很像我们Python中面向对象的继承, 通过结构体嵌套+匿名字段模拟出子类继承父类,子类可以直接调用父类中的属性或方法。
还是以上面的程序为例,如果Person
结构体内的字段,和Hobby
结构体内的字段重名,又是如何取值呢?
package main
import "fmt"
type Person struct {
Name string
Age int
Hobby
}
type Hobby struct {
Id int
Name string
}
func main() {
var p Person = Person{Name: "lqz", Age: 18, Hobby: Hobby{Id: 1, Name: "足球"}}
fmt.Println("name of Person is",p.Name) //子类和父类字段重名,优先使用自己的,类似于面向对象的派生,子类重写了父类的属性
fmt.Println("name of Hobby is",p.Hobby.Name) //打印出Hobby的name,类似利用super()方法,继承父类的属性
}
上面的程序输出为:
name of Person is lqz
name of Hobby is 足球
11 导出结构体和字段
如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。
让我们使用自定义包,编写一个程序来更好地去理解它。
在Go工作区根目录test_demo下创建一个mypackage包,包中创建一个s1.go文件,声明一个结构体。其中字段Name为大写字母开头,字段age为小写字母开头,在同一个包内,其他函数可以访问结构内所有字段。
package mypackage
type Person struct {
Name string
age int
}
结构体名和字段名都只有大写字母开头才能被其他包访问到,在main包内导入mypackage包下的Person结构体,如果我们试图访问未导出的字段 age
,编译器会报错。
package main
import (
"fmt"
"test_demo/mypackage"
)
func main() {
var p mypackage.Person = mypackage.Person{Name: "lqz"}
fmt.Println(p)
}
12 结构体相等性(Structs Equality)
结构体是值类型,值类型可以直接使用 ==
比较两个变量, 引用类型变量只能跟 nil
用 ==
比较。
如果它的每一个字段都是可比较的(每个字段都是值类型),则该结构体也是可比较的。 如果结构体包含不可比较的字段(字段为引用类型),则结构体变量也不可比较。
如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
package main
import (
"fmt"
)
type name struct {
firstName string
lastName string
}
func main() {
name1 := name{"Steve", "Jobs"}
name2 := name{"Steve", "Jobs"}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
}
name3 := name{firstName:"Steve", lastName:"Jobs"}
name4 := name{}
name4.firstName = "Steve"
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}
在上面的代码中,结构体类型 name
包含两个 string
类型。由于字符串是值类型,可比较,因此可以比较两个 name
类型的结构体变量。
上面代码中 name1
和 name2
相等,而 name3
和 name4
不相等。该程序会输出:
name1 and name2 are equal
name3 and name4 are not equal
如果结构体包含不可比较的字段,则结构体变量也不可比较。
package main
import (
"fmt"
)
type image struct {
data map[int]int
}
func main() {
image1 := image{data: map[int]int{
0: 155,
}}
image2 := image{data: map[int]int{
0: 155,
}}
if image1 == image2 {
fmt.Println("image1 and image2 are equal")
}
}
在上面代码中,结构体类型 image
包含一个 map
类型的字段。由于 map
类型是引用类型,不可比较,因此 image1
和 image2
也不可比较。如果运行该程序,编译器会报错:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)
。
13 构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
调用构造函数
package main
import (
"fmt"
)
type person struct {
name, city string
age int
}
func newPerson(name, city string, age int) *person {
return &person{
name: name,
city: city,
age: age,
}
}
func main() {
p := newPerson("lqz", "shanghai", 18)
fmt.Println(p)
}
上面程序输出为:&{lqz shanghai 18}
14 结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
通过下面的程序,我们来实现Go语言的结构体数据与Json的序列化与反序列化。
package main
import (
"encoding/json"
"fmt"
)
// 字段大写字母开头,才能被json包访问
type Student struct {
Id int
Name string
Gender string
}
func main() {
var stu01 = Student{Id: 1, Name: "egon", Gender: "男"}
//序列化: 返回byte类型切片和error
data, err := json.Marshal(stu01)
if err != nil {
fmt.Println("json序列化出错")
}
fmt.Println(data) //[]byte
fmt.Println(string(data)) //把[]byte转为字符串
var str = `{"Id":1,"Name":"egon","Gender":"男"} ` //定义字符串
var stu02 = &Student{} //定义一个结构化类型的指针
//反序列化: 把字符串转换成[]byte类型,反序列化为一个结构体
json.Unmarshal([]byte(str), stu02)
fmt.Println(stu02)
fmt.Printf("%T", stu02)
}
上面的程序输出为:
[123 34 73 100 34 58 49 44 34 78 97 109 101 34 58 34 101 103 111 110 34 44 34 71 101 110 100 101 114 34 58 34 231 148 183 34 125]
{"Id":1,"Name":"egon","Gender":"男"}
&{1 egon 男}
*main.Student
15 结构体标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项: 为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student
结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
上面程序,为了能被json包访问,结构体Student的字段必须以大写字母开头, 转换成json格式字符串后,key就是大写开头的字段,通过使用Tag,让json序列化后,key变成小写。