Skip to content

struct

Go 语言的 struct 与其他编程语言的 class 有些类似,可以定义字段和方法,但是不可以继承

内嵌字段

Go 语言的结构体没有继承的概念,当需要 "复用" 其他结构体时,需要使用组合方式将其他结构体嵌入当前结构体

例如以下代码,结构体类型 Animal 嵌入类型 Cat 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Animal struct {
    Name string
}

func (a *Animal) SetName(name string) {
    a.Name = name
}

type Cat struct {
    Animal
}

假如现在又有另一个结构体也尝试组合 Animal 类型,如下所示;

1
2
3
type Dog struct {
    a Animal
}

那么结构体类型 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
func EmbeddedFoo() {
    c := Cat{}

    c.SetName("A")
    fmt.Printf("Name: %s\n", c.Name) // A

    c.Animal.SetName("a")
    fmt.Printf("Name: %s\n", c.Name) // a

    c.Animal.Name = "B"
    fmt.Printf("Name: %s\n", c.Name) // B

    c.Name = "b"
    fmt.Printf("Name: %s\n", c.Name) // b
}

Cat 结构体可以直接访问 Animal 的字段和方法,也可以通过隐式声明的 Animal 字段来访问

方法受体

我们一般不会严格区分方法和函数,但在介绍结构体时就要区分了

一般的函数声明如下:

1
func 函数名(参数) { 函数体 }

而方法的声明规则是:

1
func (接收者) 函数名(参数) { 函数体 }

可见方法的声明多了一个 "接收者"(官方称之为 receiver,表示方法的接收者),习惯上称其为方法受体,表示该方法作用的对象

方法主要用于为类型扩展方法。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Student struct {
    Name string
}

// 作用于 Student 的拷贝对象,修改不会反映到原对象
func (s Student) SetName(name string) {
    s.Name = name
}

// 作用于 Student 的指针对象,修改会反映到原对象
func (s *Student) UpdateName(name string) {
    s.Name = name
}

我们为 Student 类型增加了两个方法,类似地也可以给其他非结构体类型增加方法

方法 SetName() 的接收者为 Student,而 UpdateName() 的接收者为 *Student,那么二者有什么区别呢?下面我们通过一个例子来展示:

1
2
3
4
5
6
7
func Receiver() {
    s := Student{}
    s.SetName("Rainbow")
    fmt.Printf("Name: %s\n", s.Name) // empty
    s.UpdateName("Rainbow")
    fmt.Printf("Name: %s\n", s.Name) // Rainbow
}

上面函数的输出结果为:

1
2
Name: 
Name: Rainbow

可以看出,虽然 SetName() 和 UpdateName() 的执行逻辑是一样的,但接收者为 Student 的 SetName(),方法并没有成功地设置名字

接收者可以简单理解为方法的作用对象,即该方法是作用于对象还是对象指针。
如果作用于对象指针,那么方法内可以修改对象的字段;而如果作用于对象,那么相当于方法执行时修改的是对象副本

还有一种理解,就是把接收者理解为方法的特殊参数,对于接收者为对象的方法,相当于参数传递时拷贝了一份对象,方法内部修改对象不会反映到原对象中,而当接收者为对象指针时,方法修改对象时会反映到原对象中

字段标签

Go 语言的 struct 声明中允许为字段标记 Tag,如下所示:

1
2
3
4
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 的本质

(1) Tag 是 Struct 的一部分

Tag 用于标识结构体字段的额外属性,有点类似于注释。
标准库 reflect 包中提供了操作 Tag 的方法,在介绍方法前,有必要先了解一下结构体的字段是如何表示的

在 reflect 包中,使用结构体 StructField 表示结构体的一个字段:

1
2
3
4
5
6
type StructField struct {
    Name string // 字段名
    Type Type  // 字段类型
    Tag StructTag // Tag
    ...
}

可以看出,Tag 也是字段的一个组成部分。Tag 的类型为 StructTag,实际上它是一个 string 类型的别名,如下所示:

1
type StructTag string

(2) Tag 约定

Tag 本身是一个字符串,单从语义上讲,任意的字符串都是合法的。但它有一个约定的格式,那就是字符串由 key:"value" 组成

  • key: 必须是非空字符串,字符串不能包含控制字符、空格、引号、冒号;
  • value: 以双引号标记的字符串

注意: key 和 value 之间使用冒号间隔,冒号前后不能有空格,多个 key:"value" 之间由空格分开

对于上面的例子:

1
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 一般表示控制指令,具体控制指令由不同的库指定,此处不再展开介绍

(3) 获取 Tag

StructTag 提供了 Get(key string) string 方法来根据 Tag 的 key 值获取 value。
比如我们获取上例 Tag 字符串中 key 值为 json 的 value,如下所示:

1
2
3
4
5
6
7
8
func PrintTag() {
    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"))
    }
}

函数输出如下:

1
2
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)定义了字符串规则并据此演绎出了丰富的应用