结构体struct
Go语言的struct与其他编程语言的class有些类似,可以定义字段和方法,但是不可以继承
内嵌字段
Go语言的结构体没有继承的概念,当需要"复用"其他结构体时,需要使用组合的方式将其他结构体嵌入进来。
如下所示:
type Animal struct {
Name string
}
func (a *Animal) SetName(name string) {
a.Name = name
}
type Cat struct {
Animal //隐式声明
}
func main() {
cat :=&Cat{}
cat.Animal.SetName("mimi")
fmt.Println(cat.Name) //等同于 cat.Animal.Name
fmt.Println(cat.Animal.Name)
}
假如现在有另外一个结构体也要尝试组合Animal,如下:
type Dog struct {
a Animal
}
那么结构体Cat 和Dog 有什么区别呢
结构体中的字段可以显式指定也可以隐式指定。在上面的例子中,Dog结构体中的字段a为显式指定,而Cat结构体中由于内嵌了Animal,从而产生了一个隐式的同名字段。
对于类型为结构体的字段,显式指定时与其他类型没啥区别,仅代表某种类型的字段,而隐式指定时,原结构体的字段和方法看起来像是被继承过来了一样。当结构体Cat中嵌入了另外一个结构体Animal时,相当于声明了一个名为Animal的字段,此时结构体Animal中的字段和方法会被提升到Cat中,看上去和Cat原生字段和方法一样。如图所示:
方法受体
一般情况下我们不会严格区分函数和方法,但是在介绍结构体的时候就要区分了。
一般的函数声明如下:
func 函数名(参数) { 函数体 }
而方法的声明如下所示:
func (接收者)函数名(参数) {函数体}
可见方法的声明多了一个接收者(官方称之为receiver)的概念。
方法主要用于为类型扩展方法,例如
type Student struct {
Name string
}
//作用于Student的拷贝对象,修改不会影响原对象
func (s Student) SetName(name string){
s.Name = name
}
//作用于指针对象,修改会影响原对象
func (s *Student) UpdateName(name string){
s.Name = name
}
上面为Student类型增加了两个方法,类似地也可以给其他非结构体类型增加方法。
方法SetName()的接收者为Student,而UpdateName()的接收者为*Student,那么二者有什么区别呢?下面通过一个例子来展示:
func Receiver(){
s := Student{}
s.SetName("佩奇")
fmt.Printf("Name is : %s\n",s.Name) //empty
s.UpdateName("喜洋洋")
fmt.Printf("Name is : %s\n",s.Name) //喜洋洋
}
Name is :
Name is : 喜洋洋
可以看出,虽然SetName和UpdateName()执行逻辑是一样的,但是接收者为Student的SetName()方法并没有成功的设置名字。
接收者可以简单理解为方法的作用对象,即该方法是作用于对象还是对象指针。如果作用于对象指针,那么方法内可以修改对象的字段;而作用于与对象,那么相当于方法内修改的是对象副本。
还有一种理解,就是把接收者理解为方法的特殊参数,对于接收者为对象的方法,相当于参数传递时拷贝了一份对象,方法内部修改对象不会影响到原对象中,而当接收者为对象指针时,方法修改对象时会影响到原对象中。
字段标签
Go语言的struct声明中允许为字段标记Tag,如下所示:
type TypeMeta struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
其中每个字段后面两个反单引号中间的字符串就是字段的Tag。
Tag的本质
Tag是struct中的一部分
Tag用于标识结构体字段的额外属性,有点类似于注释。标准库reflect包中提供了操作Tag的方法,在介绍方法前,有必要先了解一下结构体的字段是如何表示的。
在reflect包中,使用结构体StructField表示结构体的一个字段:
type StructField struct {
Name string //字段名
Type Type //字段类型
Tag StructTag //Tag
...
}
可以看出的是,Tag也是字段的一个组成部分,Tag的类型为StructTag,实际上它是一个string类型的别名,如下所示:
type StructTag string
Tag约定
Tag本身是一个字符串,单从语义上讲,任意的字符串都是合法的。但它有一个约定的格式,那就字符串由key:"value"组成。
- key:必须是非空字符串,字符串不能包含控制字符串、空格、引号、冒号
- value:以双引号标记的字符串
需要注意的是,key和value之间使用冒号分割,冒号前后不能有空格,多个key:"value"之间由空格分开。
对于上面的例子:
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
Kind字段中的Tag包含两个key:value,分别是json:"kind,omitempty"和 protobuf:“bytes,1,opt,name=kind”。
key一般表示用途,比如json表示用于控制结构体类型与JSON格式数据之间的转换,protobuf表示用于控制序列化和反序列化。value一般表示控制指令,具体指令由不同的库指定,使用到的时候可以去参考具体的库的写法
获取Tag
StructTag提供了Get(key string) string 方法来根据Tag的key值获取value。比如获取上例子Tag字符串中key值为json的value,如下所示:
type TypeMeta struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
func main() {
t := TypeMeta{}
ty := reflect.TypeOf(t)
for i := 0; i < ty.NumField(); i++ {
fmt.Printf("Field : %s , Tag : %s\n",ty.Field(i).Name,ty.Field(i).Tag.Get("json"))
}
}
函数输出如下:
Field : Kind , Tag : kind,omitempty
Field : APIVersion , Tag : apiVersion,omitempty
实际上标准库json包中将结构体对象转成JSON字符串时也是使用的类似方法。
Tag的意义
Go语言的反射特性可以动态地给结构体成员赋值,正是因为有Tag,在赋值前可以使用Tag来决定赋值的动作。
比如,官方的encoding/json包可是将一个JSON数据"Unmarshal"进一个结构体中,此过程就使用了Tag。该包定义了一些Tag规则,只要参考该规则设置Tag,就可以将不同的JSON数据转换成结构体。
综上,对于struct而言,Tag仅仅是一个普通的字符串,而其他库(如标准库json)定义了字符串规则并据此演绎了各种丰富的应用方式。