在Go语言中,我们可以给结构体的字段加一个标签(tag),Go Language Specification 中有一段简短的描述:

A field declaration may be followed by an optional string literal tag, which becomes an attribute for all the fields in the corresponding field declaration. An empty tag string is equivalent to an absent tag. The tags are made visible through a reflection interface and take part in type identity for structs but are otherwise ignored.

文中有这样的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct {
	x, y float64 ""  // an empty tag string is like an absent tag
	name string  "any string is permitted as a tag"
	_    [4]byte "ceci n'est pas un champ de structure"
}

// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

tags的作用

tags允许我们为字段附加元信息(meta-information),这些元信息可以在反射中使用。比较常见的场景是使用tags来提供字段如何被编码为另一种格式或从另一种格式解码(或从数据库存储/检索)的转换信息,但是不局限于此,它可以被用来存储任何元信息,并且可以被其他包使用。

上边提到tag提供的元信息可以在反射中使用,在 reflect.StructTag 文档 中提到,tag的值是一个字符串,一般是一组以空格分隔的key: "value"对,比如

1
2
3
4
struct {
	Microsec  uint64 `protobuf:"1" json:"microsec"`
	ServerIP6 uint64 `protobuf:"2" json:"serverIP6"`
}

如果要在 "value" 中传递多个信息,通常用逗号(',')隔开,比如

1
ServerIP6 uint64 `protobuf:"2" json:"serverIP6",omitempty`

"value" 可以使用破折号('-')来表示不处理该字段(例如,在json的情况下,意味着不对该字段进行marshal或unmarshal)。

使用反射处理tags

我们先直接看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Timestamp struct {
	Microsec  uint64 `protobuf:"1"`
	ServerIP6 uint64 `protobuf:"2"`
}

ts := Timestamp{1644067146, 4254076641}
t := reflect.TypeOf(ts)

for _, fieldName := range []string{"Microsec", "ServerIP6"} {
  field, found := t.FieldByName(fieldName)
  if !found {
    continue
  }

  fmt.Printf("\nField: Timestamp.%s\n", fieldName)
  fmt.Printf("\tTag value : %q\n", field.Tag)
  fmt.Printf("\tValue of 'protobuf': %q\n", field.Tag.Get("protobuf"))
}

输出为

1
2
3
4
5
6
7
Field: Timestamp.Microsec
        Tag value : "protobuf:\"1\""
        Value of 'protobuf': "1"

Field: Timestamp.ServerIP6
        Tag value : "protobuf:\"2\""
        Value of 'protobuf': "2"

首先我们需要获得struct的类型,然后可以通过 Type.Field(i int)Type.FieldByName(name string) 等方法来获取字段数据。这些方法返回一个 StructField 类型的值,它描述了一个struct的字段;其中有一个StructField.Tag 类型的值叫 Tag,本质上是一个字符串,它就描述了一个标签的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type StructField struct {
	// Name is the field name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}


type StructTag string

可以使用 StructTag.Get(key string) 方法来解析一个标签值并返回指定键的值,注意这需要在定义tag的时候遵循key:"value"格式,如果是其他格式就需要自己实现解析逻辑。如果tag不存在,Get函数会返回空字符串,也就是说如果一个key的值被设置成了空字符串,使用Get是没法区分的,如果想区分,需要使用 StructTag.Lookup()

常见(用)的tags

在一些库中,我们经常能看到tags的使用,比如

  • gorm
  • json
  • yaml
  • db
  • valid

举个json的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Timestamp struct {
	Microsec  uint64 `json:"microsec"`
	ServerIP6 uint64 `json:"server_ip6"`
}

json_string := `
{
  "microsec": 12345,
  "server_ip6": 67890
}
`

ts := new(Timestamp)
json.Unmarshal([]byte(json_string), ts)
fmt.Println(ts)

newJson, _ := json.Marshal(ts)
fmt.Printf("%s\n", newJson)

输出为

1
2
&{12345 67890}
{"microsec":12345,"server_ip6":67890}