struct
Go 语言的 struct 与其他编程语言的 class 有些类似,可以定义字段和方法,但是不可以继承
内嵌字段
Go 语言的结构体没有继承的概念,当需要 "复用" 其他结构体时,需要使用组合方式将其他结构体嵌入当前结构体
例如以下代码,结构体类型 Animal 嵌入类型 Cat 中:
1 2 3 4 5 6 7 8 9 10 11 |
|
假如现在又有另一个结构体也尝试组合 Animal 类型,如下所示;
1 2 3 |
|
那么结构体类型 Cat 和 Dog 有什么区别呢?
结构体中的字段名可以显式指定也可以隐式指定。在上面的例子中,Dog 结构体中的字段 a 为显式指定,而 Cat 结构体由于内嵌了结构体 Animal,从而产生了一个隐式的同名字段
对于类型为结构体的字段,显式指定时与其他类型没有区别,仅代表某种类型的字段,而隐式指定时,原结构体的字段和方法看起来就像是被 "继承" 过来了一样
当结构体 Cat 中嵌入另一个结构体 Animal 时,相当于声明了一个名为 Animal 的字段,此时结构体 Animal 中的字段和方法会被提升到 Cat 中,看上去就像是 Cat 的原生字段和方法
Cat 结构体访问 Animal 结构体的字段和方法时有两种方式,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Cat 结构体可以直接访问 Animal 的字段和方法,也可以通过隐式声明的 Animal 字段来访问
方法受体
我们一般不会严格区分方法和函数,但在介绍结构体时就要区分了
一般的函数声明如下:
1 |
|
而方法的声明规则是:
1 |
|
可见方法的声明多了一个 "接收者"(官方称之为 receiver,表示方法的接收者),习惯上称其为方法受体,表示该方法作用的对象
方法主要用于为类型扩展方法。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们为 Student 类型增加了两个方法,类似地也可以给其他非结构体类型增加方法
方法 SetName() 的接收者为 Student,而 UpdateName() 的接收者为 *Student
,那么二者有什么区别呢?下面我们通过一个例子来展示:
1 2 3 4 5 6 7 |
|
上面函数的输出结果为:
1 2 |
|
可以看出,虽然 SetName() 和 UpdateName() 的执行逻辑是一样的,但接收者为 Student 的 SetName(),方法并没有成功地设置名字
接收者可以简单理解为方法的作用对象,即该方法是作用于对象还是对象指针。
如果作用于对象指针,那么方法内可以修改对象的字段;而如果作用于对象,那么相当于方法执行时修改的是对象副本
还有一种理解,就是把接收者理解为方法的特殊参数,对于接收者为对象的方法,相当于参数传递时拷贝了一份对象,方法内部修改对象不会反映到原对象中,而当接收者为对象指针时,方法修改对象时会反映到原对象中
字段标签
Go 语言的 struct 声明中允许为字段标记 Tag,如下所示:
1 2 3 4 |
|
其中每个字段后面两个反单引号中间的字符串就是字段的 Tag
Tag 的本质
(1) Tag 是 Struct 的一部分
Tag 用于标识结构体字段的额外属性,有点类似于注释。
标准库 reflect 包中提供了操作 Tag 的方法,在介绍方法前,有必要先了解一下结构体的字段是如何表示的
在 reflect 包中,使用结构体 StructField 表示结构体的一个字段:
1 2 3 4 5 6 |
|
可以看出,Tag 也是字段的一个组成部分。Tag 的类型为 StructTag,实际上它是一个 string 类型的别名,如下所示:
1 |
|
(2) Tag 约定
Tag 本身是一个字符串,单从语义上讲,任意的字符串都是合法的。但它有一个约定的格式,那就是字符串由 key:"value"
组成
- key: 必须是非空字符串,字符串不能包含控制字符、空格、引号、冒号;
- value: 以双引号标记的字符串
注意: key 和 value 之间使用冒号间隔,冒号前后不能有空格,多个 key:"value"
之间由空格分开
对于上面的例子:
1 |
|
Kind 字段中的 Tag 包含两个 key:"value"
对,分别是 json:"kind,omitempty"
和 protobuf:"bytes,1,opt,name=kind"
key 一般表示用途,比如 json 表示用于控制结构体类型与 JSON 格式数据之间的转换,protobuf 表示用于控制序列化和反序列化。
value 一般表示控制指令,具体控制指令由不同的库指定,此处不再展开介绍
(3) 获取 Tag
StructTag 提供了 Get(key string) string
方法来根据 Tag 的 key 值获取 value。
比如我们获取上例 Tag 字符串中 key 值为 json 的 value,如下所示:
1 2 3 4 5 6 7 8 |
|
函数输出如下:
1 2 |
|
实际上标准库 json 包中将结构体对象转换成 JSON 字符串时使用的也是类似的方法
Tag 的意义
Go 语言的反射特性可以动态地给结构体成员赋值,正是因为有 Tag,在赋值前可以使用 Tag 来决定赋值的动作
比如,官方的 encoding/json 包可以将一个 JSON 数据 "Unmarshal" 进一个结构体,此过程中就使用了 Tag。
该包定义了一些 Tag 规则,只要参考该规则设置 Tag 就可以将不同的 JSON 数据转换成结构体
综上,对于 struct 而言,Tag 仅仅是一个普通的字符串,而其他库(如标准库 json)定义了字符串规则并据此演绎出了丰富的应用