Golang 05_Golang结构体

一、结构体定义与实例化

1.1 结构体的定义

Go语言提供了一种自定义数据类型,可以封装多个已有数据类型来构成一个新的数据类型,这种数据类型叫结构体,用于描述一个事物的部分或全部特征。

  • 自定义类型 Go 语言中可以使用 type 关键字来创建(自定义)一种新的数据类型:
1
type NewType Type

上面表示的就是:将 NewType 定义为 Type 类型,通过 type 定义的 NewType 就是一种新的类型,它具有 Type 的特性。 Type可以是一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,也可以是使用struct、interface 等定义的复合类型。

  • 类型别名 Go 语言中可以使用 type 关键字来给已经存在的(基本数据类型 或 自定义的 struct、interface、func 等)类型取别名
1
type TypeAlias = Type

TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。 Type可以是一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,也可以是已经定义过(已经存在)的struct、interface等。

在整型数据类型中介绍到的 runebyte 就是类型别名,它们的底层定义如下:

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,接口定义了两个函数:

1
2
code()
debug()

接着定义了一个结构体 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. 结构体也可以直接访问其内嵌的结构体成员变量 和 方法。 嵌入结构体的成员 和 方法,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入,结构体实例访问其任意一级的嵌入结构体成员、方法时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如:
 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":"男"}
}