Go问答 [每日一译] Tags in Golang

haohongfan · 2019年06月03日 · 638 次阅读

> 原文地址: Tags in Golang

我们声明 golang struct 时可以在 struct 字段后面添加一些字符串来丰富这个字段, 这些字符串称为tag. Tags 可以被当前 package 或者包外使用. 让我们首先看看 struct 是如何声明的, 然后深入研究下 tag 本身, 最后用几个例子结束

Struct type

struct 是一系列的字段的组合. 每一个字段都由一个optional名字和requiredtype 组成

package main

import "fmt"

type T1 struct {
    f1 string
}

type T2 struct {
    T1
    f2     int64
    f3, f4 float64
}

func main() {
    t := T2{T1{"foo"}, 1, 2, 3}
    fmt.Println(t.f1)    // foo
    fmt.Println(t.T1.f1) // foo
    fmt.Println(t.f2)    // 1
}

> T1 被称为 embedded field, 因为它只有类型没有名字 (hhf: 关于这个可以看我另外一篇文章: golang 面向对象分析)

字段可以用一个类型声明多个标识符, 就像 T2 的 f3, f4 一样

golang 语言声明每个字段声明后面跟着分号, 但是我们在写的时候可以忽略分号 (hhf:https://golang.org/doc/effective_go.html#semicolons, if the newline comes after a token that could end a statement, insert a semicolon). 当多个字段放在同一行时, 需要用分号分割

package main

import "fmt"

type T struct {
    f1 int64; f2 float64
}

func main() {
    t := T{1, 2}
    fmt.Println(t.f1, t.f2)  // 1 2
}

Tag

Struct 的字段声明时, 如果后面跟着可选的tag时, 那么tag将相对应的那些字段的属性

type T struct {
    f1     string "f one" // 解释字符串
    f2     string
    f3     string `f three` // 原始字符串
    f4, f5 int64  `f four and five`
}

> tag 可以使用原始字符串或者解释字符串, 下面描述的传统格式的例子需要使用原始字符串. 原始字符串和解释字符串 > 在spec有详细解释

struct 字段同一个 type 声明多个标识符 (例如上面的 f4,f5), 那么 tag 对 f4,f5 都是有效的

Reflection

Tags 是可以通过reflect被访问到的.

package main

import (
    "fmt"
    "reflect"
)

type T struct {
    f1     string "f one"
    f2     string
    f3     string `f three`
    f4, f5 int64  `f four and five`
}

func main() {
    t := reflect.TypeOf(T{})
    f1, _ := t.FieldByName("f1")
    fmt.Println(f1.Tag) // f one
    f4, _ := t.FieldByName("f4")
    fmt.Println(f4.Tag) // f four and five
    f5, _ := t.FieldByName("f5")
    fmt.Println(f5.Tag) // f four and five
}

设置空 tag 和不使用 tag 的效果时一样的

type T struct {
    f1 string ``
    f2 string
}
func main() {
    t := reflect.TypeOf(T{})
    f1, _ := t.FieldByName("f1")
    fmt.Printf("%q\n", f1.Tag) // ""
    f2, _ := t.FieldByName("f2")
    fmt.Printf("%q\n", f2.Tag) // ""
}

Conventional format

reflect: support for struct tag use by multiple packages的介绍中允许为每个 package 设置元信息. 这提供了简单的命名空间. Tags 被格式化成 key-value 键值对, Key 可以是像json包这样的名字. 键值对可以被空格分开 (这是可选的)--key1:"value1" key2:"value2" key3:"value3". 如果使用传统格式,那么我们可以使用两种 struct tag 方法--Get or Lookup, 他们返回 key 对应的 value

Lookup返回两个值: 与 key 关联的值, bool 值 (是否找到那个 key)

type T struct {
    f string `one:"1" two:"2"blank:""`
}
func main() {
    t := reflect.TypeOf(T{})
    f, _ := t.FieldByName("f")
    fmt.Println(f.Tag) // one:"1" two:"2"blank:""
    v, ok := f.Tag.Lookup("one")
    fmt.Printf("%s, %t\n", v, ok) // 1, true
    v, ok = f.Tag.Lookup("blank")
    fmt.Printf("%s, %t\n", v, ok) // , true
    v, ok = f.Tag.Lookup("five")
    fmt.Printf("%s, %t\n", v, ok) // , false
}

Get是对LookUp的简单封装, 但是他舍弃掉了 bool 值

func (tag StructTag) Get(key string) string {
    v, _ := tag.Lookup(key)
    return v
}

> 当 Tag 不是传统格式时, 那么LookupGet不会返回指定的值 (hhf:这句话是什么意思呢 Return value of Get or Lookup is unspecified if tag doesn’t have conventional format.)

不管 tag 是任何字符串 (原始字符串或者解释字符串), 只有 value 包含在双引号之间, 那么LookUpGet会返回 key 对应的值.

type T struct {
    f string "one:`1`"
}
func main() {
    t := reflect.TypeOf(T{})
    f, _ := t.FieldByName("f")
    fmt.Println(f.Tag) // one:`1`
    v, ok := f.Tag.Lookup("one")
    fmt.Printf("%s, %t\n", v, ok) // , false
}

hhf: 从这里可以看出来, 只有 value 包含在""之中才能得到相关的值

可以在双引号中使用转义的解释字符串, 但是这个可读性要差很多

type T struct {
    f string "one:\"1\""
}
func main() {
    t := reflect.TypeOf(T{})
    f, _ := t.FieldByName("f")
    fmt.Println(f.Tag) // one:"1"
    v, ok := f.Tag.Lookup("one")
    fmt.Printf("%s, %t\n", v, ok) // 1, true
}

Conversion

当一个 struct 向另外一个转换时, 需要对应字段的类型是相同的. 但是这些字段的 tag 是被忽略的

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}

使用 struct tag 的例子

(Un) marshaling

golang 使用 tag 最多的地方可能就是marshalling 让我们看一下来自 json 包的函数 Marshal 是如何使用的

import (
    "encoding/json"
    "fmt"
)
func main() {
    type T struct {
       F1 int `json:"f_1"`
       F2 int `json:"f_2,omitempty"`
       F3 int `json:"f_3,omitempty"`
       F4 int `json:"-"`
    }
    t := T{1, 0, 2, 3}
    b, err := json.Marshal(t)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", b) // {"f_1":1,"f_3":2}
}

xml包也使用了 tag 的特性—https://golang.org/pkg/encoding/xml/#MarshalIndent.

ORM

像 gorm 广泛使用 tag example

*hhf: 我觉得最好用的 orm

Other

其他潜在的用法可能就是配置管理, struct默认值, validation, 命令行参数, 例如https://github.com/golang/go/wiki/Well-known-struct-tags

go vet

Go 编译器不强制执行结构标记的传统格式,但是去看看它是否值得使用它,例如作为 CI 管道的一部分

package main
type T struct {
    f string "one two three"
}
func main() {}
> 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

hhf: 一般 IDE 都直接提示: bad syntax for struct tag

欢迎查看我的博客原文地址

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册