聊一聊golang的结构体标签

前言

Golang的结构体标签可能每一个Gopher都在用,尤其是在json处理的地方用。比如:

1
2
3
4
5
6
type NetConf struct {
Master string `json:"master"`
Mode string `json:"mode"`
MTU int `json:"mtu"`
Debug bool `json:"debug"`
}

毋庸置疑,这个 NetConf 结构体实体变量在转换 json 的时候,Master 字段会变为 json 字符串中的 “master “。

如果你对结构体标签了解仅此不多,那可以继续往下看了。

从一个结构体标签可以从属多个字段开始说

1
2
3
4
5
6
type T struct {
f1 string "f one"
f2 string
f3 string `f three`
f4, f5 int64 `f four and five`
}

上面代码在声明的时候,f4 和 f5 是一起进行的声明,因此后边的字段标签 “f four and five” 即是 f4 字段的,也是 f5 字段的。

聪明如你,你的问题就来了。

上面代码的字段标签比较特殊,是一个字符串 “f four and five” ,我们常用在 json 处理时的字段标签往往是这样的 json:”debug”` ,那么,同样的字段标签,用在多个字段上,是否可行?

我们验证一下这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/json"
"fmt"
)

type NetConf struct {
Master string `json:"master"`
Mode string `json:"mode"`
// 下面2个字段,字段标签都是 test
MTU int `json:"test"`
Debug bool `json:"test"`
}

func main() {
n := NetConf{"master1", "mode1", 1500, true}
if d,e:=json.Marshal(n);e==nil{
fmt.Println(string(d))
}
}

代码输出:

1
{"master":"master1","mode":"mode1"}

可以看到,同一个结构体中,多个字段拥有同一个字段标签后,golang 内置的 json 解析器将直接忽略这个字段标签对应的所有字段的解析。

换句话说,对于 golang 的 json 包,同样的字段标签,不能用在多个字段上。但是,json 包不能处理,并不等于 golang 不允许,这只是个别 package 的行为,仅此而已。

虽然我们在对 NetConf 的 MTU 和 Debug 字段都设置的标签为 test,我们怎么证明编译之后,这个字段标签确实存在呢(而不是被编译器主动忽略)?

这就引出我们下一个内容。

如何拿到结构体的标签内容

还是用上面的结构体,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"reflect"
)

type NetConf struct {
Master string `json:"master"`
Mode string `json:"mode"`
MTU int `json:"test"`
Debug bool `json:"test"`
}

func main() {
n := reflect.TypeOf(NetConf{})
//打印字段MTU的标签
mtu, _ := n.FieldByName("MTU")
fmt.Println(mtu.Tag)
//打印字段Debug的标签
de, _ := n.FieldByName("Debug")
fmt.Println(de.Tag)
}

输出:

1
2
3
$ go run main.go
json:"test"
json:"test"

说明什么,同一个结构体的不同字段,确实是可以有相同的字段标签的。只是 golang 内置的 json 解析器自己处理了这种特殊情况而已(或者说,json解析器自己认为这种情况对它而言有点特殊)。

其实,结构体标签是有常规格式和非常规格式的。而,golang 的 json 包,只是用了结构体标签的常规格式而已。

json:"master" 这种就是常规格式。除此之外就是非常规格式。下面,我们重点来说。

结构体标签的【常规格式】

上面已经举例了一个常规格式了,下面再举一个

1
2
3
type T struct {
f string `one:"1" two:"2" blank:""`
}

可以看出来,这种具备一个个键值对形式组成的结构体标签,便是常规格式。

我们可以使用反射,获取结构体标签的每一个键值对内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"reflect"
)

type T struct {
f string `one:"1" two:"2" blank:""`
}

func main() {
n := reflect.TypeOf(T{})
//获取字段:f
t, _ := n.FieldByName("f")
// 获取整个结构体标签内容
fmt.Println(t.Tag)
// 获取第1个键值对内容的值,以及是否设置了值
fmt.Println(t.Tag.Lookup("one"))
// 获取第2个键值对内容的值,以及是否设置了值
fmt.Println(t.Tag.Lookup("two"))
// 获取不存在的键的值,以及是否设置了值
fmt.Println(t.Tag.Lookup("xxxx"))
}
1
2
3
4
5
$ go run main.go
one:"1" two:"2" blank:""
1 true
2 true
false

可以看出来,用 reflect 包的 Tag ,可以获取每一个键值对的内容,以及是否存在这个键。

另外需要特别注意:结构体标签的多个键值对之间,必须用空格分割。,不能用逗号!!!,不能用逗号!!!,不能用逗号!!!

结构体标签的键值对中,键有什么用

键的用处,通常来说,就是 package name 。比如 json 解析的时候,结构体标签的键值对中,键通常就设置为 json。值设置为要解析或泛解析为什么字段名称。啰嗦一点,再看一下示例

1
2
3
4
5
6
7
8
type T1 struct {
// json 包将 F1 字段最终解析为 json字符串内容 foo 。
F1 int `json:"foo"`
}
type T2 struct {
// json 包将 F2 字段最终解析为 json字符串内容 bar 。
F2 int `json:"bar"`
}

结构体标签对类型转换没有影响

将一个结构体的值,转换为其他类型时,需要结构体底层类型必须相同。但是字段标签不包括在内。

1
2
3
4
5
6
7
8
9
10
type T1 struct {
f int `json:"foo"`
}
type T2 struct {
f int `json:"bar"`
}
t1 := T1{10}
var t2 T2
t2 = T2(t1)
fmt.Println(t2) // {10}

结构体标签的使用场景

(Un)marshaling

在 Golang 里边,结构体标签用的最多的地方,就是编码和解码。上面我们已经举了好几个关于 json 编码和解码的例子,不再多说。json(encoding/json) 只是编码解码的一种格式而已,其实 xml(encoding/xml)也在使用结构体标签。

在HTTP表单数据处理上,比如这个包 gorilla/schema,可以将HTTP的POST表单解析为一个结构体。

ORM

ORM,是对象关系映射的缩写(Object-relation mapping),主要用在数据库操作上。Golang里边有很多类似的工具,比如:gorm 等。

go vet

golang 的编译器对结构体标签的格式并不会强制检查其是否是合法的键值对。但是,go vet 工具会做检查。所以,为了程序的健壮性和正确性,我们可以用 go vet 作为 CI pipeline 的一部分,用在编译之前。

前面我提到,golang的结构体标签的键值对之间用空格分割,如果你一不小心用逗号分隔,还忘了这个事情,后续程序出问题,还真的无法立刻就能排查出来,这个时候,go vet 用处就很大了。

1
2
3
4
5
package main
type T struct {
f string "one two three"
}
func main() {}
1
2
> go vet tags.go
tags.go:4: struct field tag `one two three` not compatible with reflect.StructTag.Get: bad syntax for struct tag pair

其他

结构体标签还有很多其他用处。比如:配置管理、结构体字段的默认值、校验、命令行参数描述等等。golang 官方仓库的wiki中也有一些说明,有兴趣的可以看一下: Well-known-struct-tags

Donate comment here