Golang的结构体标签与反射

一、结构体标签

结构体标签是附加在结构体字段上的小块元数据字符串,格式为 key:"value",其中 key 是标签名,value 是标签的值。一个字段可以有多个标签,标签之间用空格分隔。

type User struct {
    Name string `json:"name" xml:"user_name"`
    Age  int    `json:"age" xml:"user_age"`
}

在这个例子中,NameAge 字段都有两个标签,分别用于JSON和XML的序列化

二、结构体标签的读取

要读取结构体字段的标签,可以使用reflect包中的Field方法获取结构体的字段,然后使用Tag方法获取标签字符串。

import (
    "fmt"
    "reflect"
)

type MyStruct struct {
    Name string `这是个名字标签`                         // 不推荐
    id   int    `desc:"这是id标签" sort:"这个字段可以用来排序"` // 推荐使用key value的方式定义标签
}

func main() {
    // 通过反射获取标签
    m := MyStruct{"小明", 1}
    r := reflect.TypeOf(m)
    for i := 0; i < r.NumField(); i++ {
        field := r.Field(i)
        tag := field.Tag
        fmt.Println(field.Name, "的标签->", tag)
    }
}

输出的结果是:

Name 的标签->  
id 的标签-> desc:"这是id标签" sort:"这个字段可以用来排序"
  • Name字段的标签是"这是个名字标签",但这个标签没有遵循key:"value"正确的格式,因此它不会被reflect包识别为有效的结构体标签。
  • id字段的标签是"desc:\"这是id标签\" sort:\"这个字段可以用来排序\"",这是一个有效的结构体标签,包含了两个键值对:descsort

三、结构体与反射

从上面的例子可以看到,Go语言的反射库reflect能够访问结构体标签信息。reflect包封装了反射相关的方法来获取任意对象的ValueType

  • reflect.TypeOf():返回变量的类型,类型信息由 reflect.Type 接口表示。
  • reflect.ValueOf():返回变量的值,值信息由 reflect.Value 表示。

四、reflect源码分析

reflect包的源码可以在src/reflect下面查看,例如 type.govalue.go,以下源代码基于 go1.23.3 版本,有删减。

4.1 reflect.Type

先看reflect.TypeOf()的实现,源代码在type.go

func TypeOf(i any) Type {
    return toType(abi.TypeOf(i))
}

func toType(t *abi.Type) Type {
    if t == nil {
       return nil
    }
    return toRType(t)
}

func toRType(t *abi.Type) *rtype {
    return (*rtype)(unsafe.Pointer(t))
}

TypeOf()函数对外提供了一个接口,用于获取任意类型i的类型信息。内部使用abi.TypeOf()函数来获取ABI层面的类型信息,然后通过reflect.toType()reflect.toRType()函数将ABI层面的类型信息转换为reflect包内部使用的*rtype类型,reflect.Type 相关的操作都是基于 *rtype实现的。

在 golang 的反射中, 有两个可以表示类型的关键字,KindType 。go 中的基本类型总共 26 种,在反射中定义的枚举如下:

type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Pointer
    Slice
    String
    Struct
    UnsafePointer
)

4.2 reflect.Value

再来看reflect.ValueOf()的实现,源代码在value.go

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value {
    if i == nil {
       return Value{}
    }
    return unpackEface(i)
}

func unpackEface(i any) Value {
    e := (*abi.EmptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.Type
    if t == nil {
       return Value{}
    }
    f := flag(t.Kind())
    if t.IfaceIndir() {
       f |= flagIndir
    }
    return Value{t, e.Data, f}
}

reflect.ValueOf()函数主要是通过unpackEface函数来从接口值中提取具体的值,并返回对应的Value

unpackEface函数处理了从空接口到具体值的转换,包括类型信息和数据指针的提取,以及Value对象的构造。

五、小结

通过reflect.Typereflect.Value,Go提供了反射机制,允许程序在运行时检查和操作类型的信息,包括结构体的标签。解析结构体标签的步骤如下:

  1. 获取结构体类型信息(reflect.Type):通过reflect.TypeOf()函数实现,它接受一个接口类型的参数,并返回该参数的动态类型信息。
type MyStruct struct {
    Field1 string `json:"field1"`
    Field2 int    `json:"field2"`
}

var myStruct MyStruct
t := reflect.TypeOf(myStruct) // 获取MyStruct的类型信息
  1. 遍历结构体字段:使用NumField()方法获取结构体字段的数量,然后通过Field(i)方法遍历每个字段,其中i是字段的索引。
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
}
  1. 获取字段标签(reflect.StructField):对于每个字段,使用StructField.Tag获取字段的标签字符串。Tag是一个reflect.StructTag类型,它包含了字段的标签信息。
tag := field.Tag // 获取字段的标签
  1. 解析标签:使用StructTag.Get(key)方法可以获取指定键的标签值。
if tag := field.Tag; tag != "" {
    jsonTag := tag.Get("json") // 获取json标签的值
}