一、结构体定义与实例化
1.1 结构体的定义
Go语言提供了一种自定义数据类型,可以封装多个已有数据类型来构成一个新的数据类型,这种数据类型叫结构体,用于描述一个事物的部分或全部特征。
- 自定义类型
Go 语言中可以使用 type 关键字来创建(自定义)一种新的数据类型:
上面表示的就是:将 NewType 定义为 Type 类型,通过 type 定义的 NewType 就是一种新的类型,它具有 Type 的特性。
Type可以是一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,也可以是使用struct、interface 等定义的复合类型。
- 类型别名
Go 语言中可以使用 type 关键字来给已经存在的(基本数据类型 或 自定义的 struct、interface、func 等)类型取别名
TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。
Type可以是一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,也可以是已经定义过(已经存在)的struct、interface等。
在整型数据类型中介绍到的 rune
和 byte
就是类型别名,它们的底层定义如下:
1
2
|
type byte = uint8
type rune = int32
|
go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由一个或多个任意类型的值聚合成的实体,每个值都称为结构体的成员。
结构体的成员也可以称为字段,每个字段有如下属性:
- 字段名必须唯一;
- 字段拥有自己的类型和值;
- 结构体的字段类型可以是基本数据类型、指针、切片、Map、chan、结构体(其它结构)、interface 或 字段所在结构体的指针类型;
Tips: 如果结构体的字段类型是指针、slice 和 map、chan、interface等,其零值都是 nil ,即还没有分配空间,如果需要使用这样的字段,需要先初始化,才能使用。
使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
1
2
3
4
5
6
7
8
9
|
// type + struct 定义结构体,
// 结构体首字母可以大写也可以小写,
// 大写表示这个结构体是公有的(可导出),在其它的包里可以使用,
// 小写表示这个结构体是私有的,只有这个包里面才能使用。
type Person struct {
name string
age int
sex string
}
|
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。
1.2 结构体初始化
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体的 实例 与 实例之间的内存区域是完全能独立的。
Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。
在 Go 语言中当一个变量被声明的时候,系统会自动初始化它的默认值,比如 int 被初始化为 0,指针为 nil。
在Go语言中,访问结构体指针的成员变量时可以继续使用 .
,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name
形式转换为 (*ins).Name
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
// 实例化结构体的7种方法
package main
import "fmt"
type Person struct {
name string
age int
sex string
}
func main() {
// 实例化结构体方法汇总
// 第一种方法,通过 var 声明结构体,会为结构体类型的数据分配内存,并零值化内存, p1 的类型是 Person
var p1 Person
p1.name = "张三"
p1.age = 20
p1.sex = "男"
fmt.Printf("%v %T\n", p1, p1) // {张三 20 男} main.Person
// 打印结构体的值 最好加上 # 这样能显示全部信息
fmt.Printf("%#v %T\n", p1, p1) // main.Person{name:"张三", age:20, sex:"男"} main.Person
//第二种方法,使用字面量初始化,值必须以字段在结构体定义时的顺序给出,p2 的类型是 Person
p2 := Person{
"赵四",
20,
"男",
}
fmt.Printf("%#v %T\n", p2, p2) // main.Person{name:"赵四", age:20, sex:"男"} main.Person
// 第三种方法,使用字面量初始化, 是在值前面加上了字段名和冒号,
// 这种方式下值的顺序不必一致,并且某些字段还可以被忽略掉,
// p3 的类型是 Person
p3 := Person{
age: 11,
sex: "nv",
name: "赵麻子",
}
fmt.Printf("%#v %T\n", p3, p3) // main.Person{name:"赵麻子", age:11, sex:"nv"} main.Person
// 表达式 new(Person) 和 &Person{} 是等价的。
// 第四种方法,使用 new() 函数实例化给一个结构体变量分配内存,并零值化内存, p4 的类型是 *Person
// Golang 使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name。
var p4 = new(Person)
p4.name = "李四" // 在 Golang 中支持对结构体指针直接使用.来访问结构体的成员
p4.age = 21 // p2.age =21 其实在底层是(*p2).age = age
p4.sex = "男"
fmt.Printf("%#v %T\n", p4, p4) // &main.Person{name:"李四", age:21, sex:"男"} *main.Person
// 第五种方法,等效于 new(Person), p5 的类型是 *Person
// 在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
p5 := &Person{} // 一样是 指针
p5.name = "王五"
p5.age = 22
p5.sex = "女"
fmt.Printf("%#v %T\n", p5, p5) // &main.Person{name:"王五", age:22, sex:"女"} *main.Person
// 第六种方法 ,是一种简写,底层仍会调用 new(),这里值的顺序必须按照字段顺序来写,p6 的类型是 *Person
p6 := &Person{
"赵四",
20,
"男",
}
fmt.Printf("%#v %T\n", p6, p6) // &main.Person{name:"赵四", age:20, sex:"男"} *main.Person
// 第七种方法,是一种简写,底层仍会调用 new(),同样它也可以使用在值前面加上字段名和冒号的写法,
// 这种方式下值的顺序不必一致,并且某些字段还可以被忽略掉,p7 的类型是 *Person
p4 := &Person{
age: 30,
name: "郑六",
sex: "男", // 注意每个结尾都有 , 号
}
fmt.Printf("%#v %T\n", p4, p4) // &main.Person{name:"郑六", age:30, sex:"男"} *main.Person
}
|
取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type Command struct {
Name string
Var *int
Comment string
}
func NewCommand(version int) *Command {
return &Command{
Name: "version",
Var: &version,
Comment: "show version",
}
}
func main() {
c := NewCommand(2)
fmt.Println(*c.Var)
}
|
1.3 匿名结构体
匿名结构体 没有类型名称,无需通过 type
关键字定义就可以直接使用。
匿名结构体的初始化写法由:结构体定义 和 键值对初始化两部分组成,结构体定义时没有结构体类型名称,只有字段 和 类型定义,键值对初始化部分由可选的多个键值对组成:
1
2
3
4
5
6
7
8
9
10
|
func main() {
s := struct {
Name string
Age int
}{
"王五",
18,
}
fmt.Println(s)
}
|
使用匿名结构体的例子
在本示例中,使用匿名结构体的方式定义和初始化一个消息结构,这个消息结构具有消息标示部分(ID)和数据部分(data),打印消息内容的 printMsg() 函数在接收匿名结构体时需要在参数上重新定义匿名结构体,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() {
msg := &struct {
id int
name string
}{
15,
"hahh",
}
printMsgType(msg)
}
// printMsgType 因为类型没有使用type定义,所以需要在每次用到的地方进行定义
func printMsgType(msg *struct{
id int
name string
}) {
fmt.Printf("%T\n", msg)
}
|
匿名结构体的类型名是结构体包含字段成员的详细描述,匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。
二、结构体方法和接收者
2.1 结构体方法
在 go 语言中,没有类的概念,但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。
方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其它语言中的 this 或者 self。
方法与函数区别:函数不属于任何类型,方法属于特定类型。函数没有接收者,方法有接收者。
结构体方法的定义格式如下:
1
2
3
|
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
|
- 接收者变量:接收者中的参数变量在命名的时候,官方建议使用接收者类型名称首字母的小写,而不是使用self、this这类的命名。例如,Person类型的接收者变量应该命名为p
- 接收者类型:接收者类型和参数相似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type Person struct {
Name string
Age int8
}
func newPerson(name string, age int8) *Person {
return &Person{
Name: name,
Age: age,
}
}
func (p Person) Freedom() {
fmt.Printf("%v 向往自由\n", p.Name)
}
func main() {
ggbond := newPerson("GGBond", 18)
ggbond.Freedom()
}
|
2.2 结构体方法的接收者
接收者变量: 接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Person 类型的接收者变量应该命名为 p,Connector 类型的接收者变量应该命名为 c 等。
接收者类型: 接收者类型和参数类似,可以是值类型或指针类型。
值类型的接收者: 发生值拷贝产生副本,方法内部对接收器的修改不会影响到原始值、修改只在方法内生效;值接收者可以接收该类型的值和指针调用;
Tips:Go 语言会在代码运行时将接收者的值复制一份,在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
指针类型的接收者: 接收者是指针也是拷贝产生副本,只是拷贝的是指针(内存地址),方法内部对接收者 指针指向的对象的修改会影响到原始值(特别注意,如果在方法内部 对接收者 指针变量 的指向 进行修改,从此刻开始将不会再影响方法的调用者对象);指针接收者可以接收类型的值和指针,如果是值,会被转为指针。
Tips:指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其它语言中面向对象中的 this 或者 self。
在调用方法的时候,
- 值类型的变量,可以调用该类型的值接收者方法,也可以调用指针接收者方法;
- 指针类型的变量,可以调用该类型的指针接收者方法,也可以调用值接收者方法;
也就是说,不管实现的方法的接收者是 值类型 还是 指针类型,该类型的值对象 和 指针对象 这些实现了的方法,不必严格限制方法接收者的类型是 值类型 还是 指针类型。
实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作(通过语法糖起作用的),用一个表格来呈现:
- |
值接收者 |
指针接收者 |
值类型调用者 |
方法会使用调用者的一个副本,类似于“传值” |
使用值的引用来调用方法,obj.func() 实际上是 (&obj).func() |
指针类型调用者 |
Go 里边对于 (Type)Method 的方法,编译器会自动为它实现 (*Type)Method 方法 |
实际上也是“传值”,这个值是指针,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
package main
import "fmt"
type Car struct{
Name string
}
// 值类型的接收者
func (c Car) PrintInfo(){
fmt.Printf("%v\n",c.Name)
}
// 值类型的接收者
func (c Car) SetName(name string){
// c 是传入方法的一个副本变量,修改 c 的值操作只是针对副本,无法修改接收者变量本身。
c.Name = name
}
// 指针类型的接收者
func (c *Car) SetInfo(name string){
// c 是传入方法的一个指针变量,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
c.Name = name
}
// 指针类型的接收者
// 如果在方法内部 对接收者 指针变量 的指向 进行修改,从此刻开始将不会再影响方法的调用者对象
func (c *Car) ChangeReceiverSelfe(name string){
// 改变 c 指针的指向,使其指向一个新的地址
c = new(Car) // 从此刻开始,对 c 的操作将不会再影响方法的调用者对象
c.Name = name
}
func main() {
c := Car{"奔驰"} // 值类型对象
c.PrintInfo()
c.SetName("BMW") // 值类型的接收者修改操作只是针对副本,无法修改接收者变量本身。
c.PrintInfo()
c.SetInfo("BMW") // 会转换为 (&c).SetInfo指针类型(值转换为指针)的接收者,在方法结束后,修改都是有效的。
c.PrintInfo()
c1.ChangeReceiverSelfe("奔驰") // 在方法内部 对接收者 指针变量 的指向 进行修改,从此刻开始将不会再影响方法的调用者对象
c1.PrintInfo()
c1 := &Car{"奔驰"} // 指针类型对象
c1.PrintInfo() // 会转换为 (*c1).PrintInfo()
c1.SetName("BMW") // 会转换为 (*c1).SetName("BMW") 值类型(指针转换为值)的接收者修改操作只是针对副本,无法修改接收者变量本身。
c1.PrintInfo() // 会转换为 (*c1).PrintInfo()
c1.SetInfo("BMW") // 指针类型的接收者,在方法结束后,修改都是有效的。
c1.PrintInfo() // 会转换为 (*c1).PrintInfo()
c1.ChangeReceiverSelfe("奔驰") // 在方法内部 对接收者 指针变量 的指向 进行修改,从此刻开始将不会再影响方法的调用者对象
c1.PrintInfo() // 会转换为 (*c1).PrintInfo()
}
/* 输出:
奔驰
奔驰
BMW
奔驰
奔驰
BMW
BMW
*/
|
*Golang 中对于值接收者 (t Type)Method 的方法,编译器会自动为它实现指针接收者 (t Type)Method 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// t1.go
package car
type Car struct{
Name string
}
// 值类型的接收者
func (c Car) SetName(name string){
// c 是传入方法的一个副本变量,修改 c 的值操作只是针对副本,无法修改接收者变量本身。
c.Name = name
}
// 指针类型的接收者
func (c *Car) SetInfo(name string){
// c 是传入方法的一个指针变量,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
c.Name = name
}
|
1
2
3
4
5
|
192:CodeTest mac$ go tool compile -l -p t1 t1.go
192:CodeTest mac$ go tool nm t1.o | grep " T "
1b7d T t1.(*Car).SetInfo
203f T t1.(*Car).SetName # 可以看出,编译器自动为 Car 实现了指针接收者 (c *Car)SetName() 方法
1b7c T t1.Car.SetName
|
值接收者 和 指针接收者方法实现接口时的约束
Tips: 接口(interface)将在下一章介绍。
无论方法接收者(实现接口)是值接收(对象),还是是指针接收(对象指针),都可以用调用者直接调用方法。此时调用者不受是值还是指针的限制。
也就是说,无论接收者是值类型还是指针类型,都可以通过值类型或者指针类型调用,原因是这里有寻址及解引用的隐形操作。
- 全部为值接收者实现的接口,其指针也自动实现了该接口,可以使用值接口对象(
var if interface{...} = Type{}
)调用接口方法、也可以使用指针接口对象(var if interface{...} = &Type{}
)调用接口方法;
- 包含有指针接收者(至少一个方法接收者为指针)实现的接口,只有其指针实现了该接口,值接口对象(
var if interface{...} = Type{}
)不能调用接口方法、只有指针接口对象(var if interface{...} = &Type{}
)才可以调用接口方法;
如下示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import "fmt"
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() { // 有至少一个指针接收者实现的接口方法
fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
var c coder = &Gopher{"Go"} // 调用者为 接口,只能指针接口对象 可以调用接口方法
c.code()
c.debug()
}
|
上述代码里定义了一个接口 coder,接口定义了两个函数:
接着定义了一个结构体 Gopher,它实现了coder 接口的两个方法,一个值接收者,一个指针接收者。
在 main 函数里通过接口类型的变量调用了定义的两个函数。
运行一下,结果:
1
2
|
I am coding Go language
I am debuging Go language
|
但是如果把 main 函数的第一条语句换一下:
1
2
3
4
5
|
func main() {
var c coder = Gopher{"Go"} // 调用者为 接口,值接口对象,不可以调用接口方法
c.code()
c.debug()
}
|
运行将报错:
1
2
|
# command-line-arguments
./t.go:23:16: cannot use Gopher{…} (value of type Gopher) as coder value in variable declaration: Gopher does not implement coder (method debug has pointer receiver)
|
两种方式调用接口方法的差别:
- 第一次是将 &Gopher 赋给了 coder,可以运行,这是因为 编译器自动为 Gopher 实现了指针接收者 (p *Gopher) code() 方法;在调用 方法时,coder接口类型的变量 c 需要隐式转为 *Gopher 类型对象后才进行方法的调用。
- 第二次则是将 Gopher 赋给了 coder,第二次报错是说,Gopher 没有实现 coder 接口,因为 Gopher 类型并没有实现 debug 方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// t1.go
package car
type ICarer interface {
SetName(name string)
SetInfo(name string)
}
type Car struct{
Name string
}
// 值类型的接收者
func (c Car) SetName(name string){
// c 是传入方法的一个副本变量,修改 c 的值操作只是针对副本,无法修改接收者变量本身。
c.Name = name
}
// 指针类型的接收者
func (c *Car) SetInfo(name string){
// c 是传入方法的一个指针变量,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
c.Name = name
}
type BlueCar struct{
Name string
}
// 值类型的接收者
func (c BlueCar) SetName(name string){
// c 是传入方法的一个副本变量,修改 c 的值操作只是针对副本,无法修改接收者变量本身。
c.Name = name
}
// 值类型的接收者
func (c BlueCar) SetInfo(name string){
// c 是传入方法的一个副本变量,修改 c 的值操作只是针对副本,无法修改接收者变量本身。
c.Name = name
}
type RedCar struct{
Name string
}
// 指针类型的接收者
func (c *RedCar) SetName(name string){
// c 是传入方法的一个指针变量,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
c.Name = name
}
// 指针类型的接收者
func (c *RedCar) SetInfo(name string){
// c 是传入方法的一个指针变量,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
c.Name = name
}
|
查看编译器对不同类型接收者接口方法的处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
192:SourceCodeTest mac$ go tool compile -l -p t1 t1.go
192:SourceCodeTest mac$
192:SourceCodeTest mac$ go tool nm t1.o | grep " T "
62c5 T t1.(*BlueCar).SetInfo
6351 T t1.(*BlueCar).SetName
50e8 T t1.(*Car).SetInfo
6239 T t1.(*Car).SetName
51a8 T t1.(*RedCar).SetInfo
5149 T t1.(*RedCar).SetName
5148 T t1.BlueCar.SetInfo
5147 T t1.BlueCar.SetName
50e7 T t1.Car.SetName
612f T t1.ICarer.SetInfo
61b4 T t1.ICarer.SetName
|
这是因为,接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。
所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。
但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。
实现接口时约束:
- 如果定义的是值类型接收者的方法
(Type)Method
,则该类型会隐式的声明一个指针类型接收者的方法 (*Type)Method
;
- 如果定义的是指针类型接收者的方法
(*Type)Method
,则不会隐式声明一个值类型接收者的方法 (Type)Method
;
什么时候使用指针类型的接收者:
1)需要修改接收者中的值
2)接收者拷贝代价比较大的大对象
3)保证一致性,在同一个文件中,如果有某个方法使用了指针接收者,那么其它的方法也建议使用指针接收者
2.3 任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,可以基于内置的int类型使用type关键字可以定义新的自定义类型,然后为自定义新类型添加方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
|
Tips: 非本地类型不能定义方法,也就是说不能给别的包的类型定义方法。
三、结构体的匿名字段
3.1 结构体的匿名字段简介
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为 匿名字段。
结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以内嵌结构体(下一节介绍)。
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package main
import "fmt"
type User struct {
Name string
Age int
Gender string
string // string类型的匿名字段
int // int类型的匿名字段
Address // 结构体类型的匿名字段
}
type Address struct {
Province string
City string
}
func main() {
var u1 User
u1.Name = "张三"
u1.Gender = "男"
u1.string = "string类型的匿名字段"
u1.int = 100 // int类型的匿名字段
u1.Address.City = "昆明" // 匿名字段默认使用类型名作为字段名
u1.Province = "云南" // 匿名字段可以省略,但注意多个匿名字段下有相同字段名,
// 会编译失败,所以建议不采用省略写法
fmt.Printf("Name:%v, Address: %v省%v市, unknow fields: %v, %v",
u1.Name, u1.Address.City, u1.Province, u1.string, u1.int)
}
|
四、结构体匿名嵌套与组合
4.1 结构体匿名嵌套与组合
一个结构体中可以匿名嵌套(包含)另一个 结构体 或 结构体指针。通过结构体匿名嵌套,内部结构体的 属性 和 方法 可以为外部结构体所有,就好像是外部结构体自己的一样。此外,外部结构体还可以定义自己的 属性 和 方法,甚至可以定义与 内部结构体相同的 属性 和 方法,这样内部结构体的重名 属性 和 方法 就将会被外部对应的属性 和 方法 “屏蔽”。
可以粗略地将结构体 匿名嵌套 和 面向对象语言中的继承概念相比较,它被用来模拟类似继承的行为。Go语言中的继承是通过匿名内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。
go语言的结构体匿名内嵌有如下特性:
- 匿名内嵌结构体的字段名是它的类型名
匿名内嵌结构体字段可以通过详细的字段进行一层层的访问,内嵌结构体的字段名就是它的类型名。
- 结构体也可以直接访问其内嵌的结构体成员变量 和 方法。
嵌入结构体的成员 和 方法,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入,结构体实例访问其任意一级的嵌入结构体成员、方法时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
package main
import "fmt"
type A struct {
xa int
}
func (a A)funcA() {
fmt.Println("struct A's funcA running...., xa:", a.xa)
}
type B struct {
xb uint
A
}
func (b B)funcB() {
fmt.Println("struct B's funcB running...., xb:", b.xb)
}
type C struct {
xc uint64
B
}
func (c C)funcC() {
fmt.Println("struct C's funcC running...., xc:", c.xc)
}
func main() {
var c C
c.xc = 10
c.funcC() // struct C's funcC running...., xc: 10
// 匿名内嵌结构体字段可以通过详细的字段进行一层层的访问,内嵌结构体的字段名就是它的类型名。
c.B.xb = 20
c.B.A.xa = 30
c.B.funcB() // struct B's funcB running...., xb: 20
c.B.A.funcA() // struct A's funcA running...., xa: 30
fmt.Printf("c: %+v\n", c) // c: {xc:10 B:{xb:20 A:{xa:30}}}
// 嵌入结构体的成员 和 方法,可以通过外部结构体的实例直接访问。
c.xb = 200
c.xa = 300
c.funcB() // struct B's funcB running...., xb: 200
c.funcA() // struct A's funcA running...., xa: 300
fmt.Printf("\nc: %+v\n", c) // c: {xc:10 B:{xb:200 A:{xa:300}}}
}
|
结构体匿名嵌套的命名冲突
在结构体嵌套时,可能会出现内层结构体的 成员或方法 与 外层结构体新定义的 成员或方法 重名 或者 内层多个嵌套结构体之间存在 成员或方法 重名 的情况,此时,外层结构体实例将不能再直接访问 这些重名的 内层结构体 成员或方法,而是需要提供字段名逐层访问指定内嵌结构体的成员 或方法,如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
package main
import "fmt"
type A struct {
x, y int
}
func (a A)funcX(runner string) {
fmt.Printf("runner: %-10s| struct A's funcX running...., x: %v\n", runner, a.x)
}
func (a A)funcY(runner string) {
fmt.Printf("runner: %-10s| struct A's funcY running...., y: %v\n", runner, a.y)
}
type B struct {
x, y string // B的 x、y 字段覆盖(隐藏)了 A的 x、y 字段(即使二者类型不相同),但二者的内存空间都会分配并保留
z int64
A
}
func (b B)funcX(runner string, arg int) { // 覆盖了 A 的 funcX 方法,即使 二者的签名不同
fmt.Printf("runner: %-10s| struct B's funcX running...., x: %v, arg: %v\n", runner, b.x, arg)
}
func (b B)funcY(runner string) { // 覆盖了 A 的 funcY 方法
fmt.Printf("runner: %-10s| struct B's funcY running...., y: %v\n", runner, b.y)
}
func (b B)funcZ(runner string) {
fmt.Printf("runner: %-10s| struct B's funcZ running...., z: %v\n", runner, b.z)
}
type D struct {
z int64
}
func (d D)funcX(runner string, arg string) {
fmt.Printf("runner: %-10s| struct D's funcX running...., z: %v, arg: %v\n", runner, d.z, arg)
}
func (d D)funcZ(runner string, arg int) {
fmt.Printf("runner: %-10s| struct D's funcZ running...., z: %v, arg: %v\n", runner, d.z, arg)
}
// 在 C 中的成员变量,C 的 x 与 从 B、A 继承来的 x 冲突(重名), C 的 x 将 C 从 B、A 继承来的 x 覆盖掉,
// 此时 C 的对象 c.x 访问是 C 的 x,从 B、A 继承来的 x 不可直接访问,需要使用 c.B.x 和 c.B.A.x 进行访问;
// 在 C 中的成员变量,C 从 B 继承来的 y 与 从 A 继承来的 y 冲突, C 从 B 继承的 y 将 从 A 继承的 y 覆盖掉,
// 此时 C 的对象 c.y 访问是 C 从 B 继承来的 x,从 A 继承来的 x 不可直接访问,需要使用 c.A.x 或 c.B.A.x 进行访问;
// 在 C 中的成员变量,C 从 B 继承来的 z 与 从 D 继承来的 z 冲突, B 和 D 没有嵌套继承关系,
// 此时 C 的对象 不能直接访问 c.z,需要使用 c.B.z 或 c.D.z 指明具体访问哪个嵌套结构体继承来的 z;
// 在 C 中的成员方法,C 的 funcX 与 从 B、A、D 继承来的 funcX 冲突, C中重写的 funcX 将其继承来的 duncX 覆盖掉,
// 此时 C 的对象 c.funcX() 调用是 C 重写的 funcX,从 B、A、D 继承来的 funcX 不可直接调用,
// 需要使用 c.B.funcX()、c.B.A.funcX()、c.D.funcX() 进行调用;
// 在 C 中的成员方法,C 从 B 继承来的 funcY 与 从 A 继承来的 funcY 冲突, C 从 B 继承的 funcY 将
// 从 A 继承的 funcY 覆盖掉,此时 C 的对象 c.funcY() 调用的是 C 从 B 继承来的 funcY,
// 从 A 继承来的 funcY 不可直接调用,需要使用 c.A.funcY() 或 c.B.A.funcY() 进行访问;
// 在 C 中的成员方法,C 从 B 继承来的 funcZ 与 从 D 继承来的 funcZ 冲突, B 和 D 没有嵌套继承关系,
// 此时 C 的对象不能直接调用 funcZ,需要使用 c.B.funcZ() 或 c.D.funcZ() 指明具体调用哪个从嵌套结构体继承来的 funcZ;
type C struct {
x bool // C 中 x 字段覆盖(隐藏)了从B、A继承来的 x 字段(即使三者的类型不相同),但三者的内存空间都会分配并保留
B // C 从 B 继承的 y 字段覆盖(隐藏)了 从 A的 x 字段(即使二者的类型不相同),但二者的内存空间都会分配并保留
D // C 从 B、D 继承来的 z 字段发生冲突,不可直接访问
}
func (c C)funcX(runner string) { // 覆盖了 B、A、D 的 funcX 方法,即使这些方法的签名不同
fmt.Printf("runner: %-10s| struct C's funcX running...., x: %v\n", runner, c.x)
}
func main() {
fmt.Printf("Struct B =========================================\n")
var b B
b.x = "B.x" // 访问 struct B 中的成员 x,A 的 x 成员被覆盖
b.y = "B.y" // 访问 struct B 中的成员 y,A 的 y 成员被覆盖
b.z = 10
b.funcX("obj b", 100) // 调用 struct B 中的 funcX 方法,A 的 funcX 方法被覆盖
b.funcY("obj b") // 调用 struct B 中的 funcY 方法,A 的 funcY 方法被覆盖
b.A.x = 10 // 访问 struct B 从 A 继承来的成员 x
b.A.y = 11 // 访问 struct B 从 A 继承来的成员 y
b.A.funcX("obj b.A")// 调用 struct B 隐藏的从 A 继承来的方法 funcX
b.A.funcY("obj b.a")// 调用 struct B 隐藏的从 A 继承来的方法 funcY
fmt.Printf("\nb mem struct: %+v\n", b) // 被覆盖的字段的内存空间都会分配并保留
fmt.Printf("\nStruct C=========================================\n")
var c C
c.x = true // 访问 struct C 中的成员 x,B、A、D 的 x 成员被覆盖
c.y = "I'm C.B.y" // 访问 从 B 中继承来的成员 y,从 A 继承的 y 成员被覆盖
// c.z = "I don't know who i am" // ambiguous selector c.z,从B、D 继承的z 成员不可直接访问
c.funcX("obj c") // 调用 struct C 中的 funcX 方法,B、A、D 的 funcX 方法被覆盖
c.funcY("obj c") // 调用 从 B 中继承的 funcY 方法,A 的 funcY 方法被覆盖
// c.funcZ() // ambiguous selector c.funcZ 从B、D 继承的 funcZ不可直接调用
// 匿名内嵌结构体重名字段必须通过详细的字段名(类型名)进行一层层的访问,内嵌结构体的字段名就是它的类型名
c.B.x = "I am B.x" // 访问 C 从 B中继承来的的成员 x
c.B.y = "I am B.y" // 访问 C 从 B中继承来的的成员 y
c.B.z = 1000 // 访问 C 从 B中继承来的的成员 z
c.B.funcX("obj c.B", 456) // 调用 struct C 被隐藏的从 B 继承来的方法 funcX
c.B.funcY("obj c.B")// 调用 struct C 被隐藏的从 B 继承来的方法 funcY
c.B.funcZ("obj c.B")// 调用 struct C 从 B 继承来的方法 funcZ
c.B.A.x = 100 // 访问 C 从 A中继承来的的成员 x
c.B.A.y = 1000 // 访问 C 从 A中继承来的的成员 y
c.A.y = 10000 // 访问 C 从 A中继承来的的成员 y,与 c.B.A.y
c.B.A.funcX("obj c.B.A") // 调用 struct C 被隐藏的从 A 继承来的方法 funcX
c.B.A.funcY("obj c.B.A") // 调用 struct C 被隐藏的从 A 继承来的方法 funcX
c.A.funcY("obj c.A") // 调用 struct C 被隐藏的从 A 继承来的方法 funcX,与 c.B.A.funcY
c.D.z =188 // 访问 C 从 D 中继承来的的成员 z
c.D.funcZ("obj c.D",123) // 调用 struct C 从 D 继承来的方法 funcZ
fmt.Printf("\nc mem struct: %+v\n", c) // 被覆盖的字段的内存空间都会分配并保留
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
Struct B =========================================
runner: obj b | struct B's funcX running...., x: B.x, arg: 100
runner: obj b | struct B's funcY running...., y: B.y
runner: obj b.A | struct A's funcX running...., x: 10
runner: obj b.a | struct A's funcY running...., y: 11
b mem struct: {x:B.x y:B.y z:10 A:{x:10 y:11}}
Struct C=========================================
runner: obj c | struct C's funcX running...., x: true
runner: obj c | struct B's funcY running...., y: I'm C.B.y
runner: obj c.B | struct B's funcX running...., x: I am B.x, arg: 456
runner: obj c.B | struct B's funcY running...., y: I am B.y
runner: obj c.B | struct B's funcZ running...., z: 1000
runner: obj c.B.A | struct A's funcX running...., x: 100
runner: obj c.B.A | struct A's funcY running...., y: 10000
runner: obj c.A | struct A's funcY running...., y: 10000
runner: obj c.D | struct D's funcZ running...., z: 188, arg: 123
c mem struct: {x:true B:{x:I am B.x y:I am B.y z:1000 A:{x:100 y:10000}} D:{z:188}}
|
说明:
- 在结构体嵌套中,外层名字会覆盖内层同名名字(即使字段类型或方法签名不同,但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
- 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正;
结构体嵌套中方法的处理原理
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package main
// import "fmt"
type People struct {
name string
}
func (p *People) ShowA() {
// fmt.Println("ShowA")
p.name = "PeoA"
p.ShowB()
}
func (p *People) ShowB() {
// fmt.Println("ShowB")
p.name = "PeoB"
}
type Teacher struct {
People
age uint8
}
func (t *Teacher) ShowB() {
// fmt.Println("Teacher ShowB")
t.age = 36
}
func main() {
t := Teacher{}
t.ShowA()
}
|
1
2
3
4
5
6
7
|
192:CodeTest mac$ go tool compile -l -p main main.go
192:CodeTest mac$
192:CodeTest mac$ go tool nm main.o | grep Show | grep " T "
2c86 T t1.(*People).ShowA
2cde T t1.(*People).ShowB
3500 T t1.(*Teacher).ShowA # 代码中没有定义,由编译器自动生成
2d36 T t1.(*Teacher).ShowB
|
所以编译器为 *Teacher
生成了这样的一个包装方法:
1
2
3
|
func (t *Teacher) ShowA() {
(&(t.People)).ShowA()
}
|
可以确定 t.ShowA()
会在语法糖的作用下,转换为对 (*Teacher).ShowA()
方法的调用,而 (*Teacher).ShowA()
又会取出 People 成员的地址,作为接收者,去执行 *People 的 ShowA 方法,所以会有这样的输出。
也就是说,对于匿名嵌套继承来的方法,如果派生类型未对其重新,则派生类对象调用这类方法时,方法接收者(派生类对象)看上去像是隐式的转换为这类方法定义时的 接收者类型(父类型)对象进行调用,即 在这类方法体中,其接收者从 派生类型对象 隐式的转换为了 parent 类型对象。
明明是Teacher中内嵌了People,为什么编译器只给*Teacher生成包装方法?
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package main
import "fmt"
type A int
func (a A) Value() int {
// fmt.Println("ShowA")
return int(a)
}
func (a *A)Set(n int) {
*a = A(n)
}
type B struct {
A
b int
}
type C struct {
*A
c int
}
func main() {
a := A(0)
pa := &a
a.Set(1)
pa.Set(11)
fmt.Println(pa.Value())
b := B{A: a, b: 10}
pb := &b
b.Set(2)
pb.Set(12)
fmt.Println(pb.Value())
c := C{A: &a, c: 11}
pc := &c
c.Set(3)
pc.Set(13)
fmt.Println(pc.Value())
}
|
为了支持接口,编译器会为值接收者方法生成指针接收者的包装方法,所以 *A 多了一个 Value 的包装方法。再除去已定义的方法,剩下的是编译器生成的包装方法,可以看到,编译器把逻辑合理的方法都生成出来了。只有 B 只继承了 A 的方法,没有继承 *A 的方法。这是因为:以 B 为接收者调用方法时,方法操作的已经是 B 的副本,无法获取嵌入的 A 的原始地址;而 *A 的方法从语义上来讲,需要操作原始变量,也就是说,对于 B 而言,它继承 *A 的方法是没有意义的,所以编译器并没有给B生成Set方法。
结论:无论是嵌入值还是嵌入指针,值接收者方法始终能够被继承。而只有在能够拿到嵌入对象的地址时,才能继承指针接收者方法。
这就是编译器为组合式继承生成包装方法的规则:
4.2 结构体继承与多态
Go不是面向对象编程的语言,但可以通过结构体匿名嵌套的方式来实现面向对象的 继承 和 多态 特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
// 结构体继承
package main
import "fmt"
type Animal struct {
Name string
}
func (a *Animal) run() {
fmt.Printf("%v 在奔跑\n", a.Name)
}
func (a *Animal) eat() {
fmt.Printf("%v 在吃食物\n", a.Name)
}
type Dog struct {
Age int
Animal // 这里 嵌套了Animal,使得 Dog 继承了 Animal (继承了Animal的字段 和 方法)
}
func (d *Dog) eat() { // 子类还可以重写父类的部分或全部的方法
fmt.Printf("%v 在啃骨头。\n", d.Name)
}
func (d *Dog) info() { // 子类还还能拥有自己的方法:
fmt.Printf("这条狗名字叫:%v 年龄:%v\n", d.Name, d.Age)
}
type Cat struct {
Age int
*Animal // 这里 嵌套了 *Animal,使得 Cat 继承了 Animal (继承了Animal的字段 和 方法)
}
func (c *Cat) eat() { // 子类还可以重写父类的部分或全部的方法
fmt.Printf("%v 在吃老鼠。\n", c.Name)
}
func (d *Cat) Info() { // 子类还还能拥有自己的方法:
fmt.Printf("这只猫名字叫:%v 年龄:%v\n", d.Name, d.Age)
}
func main() {
// 结构体继承
var d Dog
d.Name = "大黄"
d.Age = 3
d.run() // 大黄 在奔跑, 这里 d 实例 继承了 Animal 的 run 方法
d.eat() // 大黄 在啃骨头。这里Dog子类重写Animal父类的 eat 方法
d.info() // 这条狗名字叫:大黄 年龄:3
c := &Cat{
Age: 2,
Animal: &Animal{
Name: "小花",
},
}
c.run() // 小花 在奔跑, 这里 c 实例 继承了 Animal 的 run 方法
c.eat() // 小花 在吃老鼠。 这里Cat子类重写Animal父类的 eat 方法
c.Info() // 这只猫名字叫:小花 年龄:2
}
|
1
2
3
4
5
6
|
func check(animal Animal) {
animal.eat()
}
// 在这个函数中就可以处理所有组合了Animal的单位类型,
// 即一个可以处理任何特定类型以及是该特定类型的派生类的通配符,
// 换句人话,啥动物都能处理。
|
在 Golang 中 多态 一般使用 接口(interface)实现。接口将在下一章进行介绍。
五、空结构体
5.1 空结构体简介
在Go语言中,空结构体 struct{}是一种特殊的数据类型,它不占用任何内存空间。
注意,空结构体虽然不包含任何字段,但是它仍然是一种类型,普通结构的所有属性同样适用于空结构,因此可以定义变量、类型别名、方法等。
空结构体的特点:
- 零内存占用: 空结构体不占用任何内存空间,这使得空结构体在内存优化方面非常有用
- 地址相同: 无论创建多少个空结构体,它们所指向的地址都相同的。
- 无状态: 由于空结构体不包含任何字段,因此它不能有状态。这使得空结构体在表示无状态的对象或情况时非常有用。
空结构体的定义
1
2
3
4
5
|
var EmptyStruct struct{}
fmt.Println(unsafe.Sizeof(EmptyStruct)) // 0
v1 := EmptyStruct{}
fmt.Println(unsafe.Sizeof(v1)) // 0
|
由于空结构体struct{}的大小为 0,所以当一个结构体中包含空结构体类型的字段时,通常不需要进行内存对齐。
1
2
3
4
5
6
7
|
type Demo1 struct {
m struct{} // 0
n int8 // 1
}
var d1 Demo1
fmt.Println(unsafe.Sizeof(d1)) // 1
|
但是当空结构体类型作为结构体的最后一个字段时,如果有指向该字段的指针,那么就会返回该结构体之外的地址。为了避免内存泄露会额外进行一次内存对齐
1
2
3
4
5
6
7
|
type Demo2 struct {
n int8 // 1
m struct{} // 0
}
var d2 Demo2
fmt.Println(unsafe.Sizeof(d2)) // 2
|
5.2 空结构体使用场景
1) 实现集合类型
Go语言本身是没有集合类型(Set),通常是使用map来替代,但有个问题,就是集合类型,只需要用到key(键),不需要用到value(值)
如果value使用bool来表示,实际会占用1个字节的空间,为了节省空间,这时空结构体就可以大显身手了。
1
2
3
4
5
6
7
8
9
|
type Set map[int]struct{} // 空结构体作为占位符,不会额外增加不必要的内存开销
func (s Set) Add(str string) {
s[str] = struct{}{}
}
func (s Set) Contains(str string) bool {
_, ok := s[str]
return ok
}
|
2) 实现空通道
场景1: 通知任务完成
在Go语言 channel 的使用场景中,常常会遇到通知型 channel,其不需要发送任何数据,只是用于协调 Goroutine 的运行,用于流转各类状态或是控制并发情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func worker(done chan struct{}) {
fmt.Print("worker running...")
// Send a value to notify that we're done.
done <- struct{}{}
}
func main() {
// Start a worker goroutine, giving it the channel to
// notify on.
done := make(chan struct{}, 1)
go worker(done)
// Block until we receive a notification from the
// worker on the channel.
<-done
}
// 由于该 channel 使用的是空结构体,因此也不会带来额外的内存开销
|
场景2: 使用空结构体+select语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 利用 time.After 实现
func main() {
done := do()
select {
case <-done:
// logic
case <-time.After(3 * time.Second):
// timeout
}
}
func do() <-chan struct{} {
done := make(chan struct{}, 1)
go func() {
// do something
// ...
done <- struct{}{}
}()
return done
}
|
场景3: 限制最大并发数
1
2
3
4
5
6
7
8
|
limits := make(chan struct{}, 2) 限制最大并发数为2
for i := 0; i < 10; i++ {
go func() {
limits <- struct{}{} // 缓冲区满了就会阻塞在这
do()
<-limits
}()
}
|
3) 实现方法接收者
使用结构体类型的变量作为方法接收者,有时结构体可以不包含任何字段属性。这种情况,可以用int或者string来替代,但它们都会占用内存空间,所以使用空结构体是比较合适的。
1
2
3
4
5
6
7
8
9
|
type Foo struct{}
func (f Foo) Eat() {
fmt.Println("foo eat")
}
func (f Foo) Run() {
fmt.Println("foo run")
}
|
六、结构体标签(Tag)与 JSON序列化
6.1 结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号"“包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
// Student 学生
type Student struct {
ID int
Gender string
Name string
}
// Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 5; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{
"Title":"101",
"Students":[
{
"ID":0,
"Gender":"男",
"Name":"stu00"
},
{
"ID":1,
"Gender":"男",
"Name":"stu01"
},
{
"ID":2,
"Gender":"男",
"Name":"stu02"
},
{
"ID":3,
"Gender":"男",
"Name":"stu03"
},
{
"ID":4,
"Gender":"男",
"Name":"stu04"
},
{
"ID":5,
"Gender":"男",
"Name":"stu05"
}
]
}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
|
6.2 结构体标签(Tag)
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
1
|
key1:"value1" key2:"value2"
|
结构体tag由一个或多个键值对组成。
键与值使用冒号分隔,值用双引号括起来。
同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项:
- 为结构体编写Tag时,必须严格遵守键值对的规则。
- 结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。
例如为Student结构体的每个字段定义json序列化时使用的Tag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//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: "GGBond",
}
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":"男"}
}
|