Go语言反射,reflection

Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。

反射的类型对象(reflect.Type)

在go语言程序中,使用reflect.TypeOf()函数可以获取任意值的类型对象(reflect.Type),程序可以通过类型对象访问任意值的类型信息,
下面通过示例来理解获取类型对象的过程:

func main() {
	var a myInt
	typeOfA := reflect.TypeOf(a)
	// Name返回直接类型的名称,Kind返回底层类型的名称
	fmt.Println(typeOfA.Name(), typeOfA.Kind())  // myInt, int
}

反射的类型Type与种类Kind

再使用反射时需要首先理解类型Type和种类Kind的区别,编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
(1)反射种类Kind的定义
Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
(2)从类型对象中获取类型名称和种类
Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串;类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。

下面的代码中会对常量和结构体进行类型信息获取。

func main() {
	type cat struct {}
	typeOfCat := reflect.TypeOf(cat{})
	fmt.Println(typeOfCat.Name(), typeOfCat.Kind())  // cat struct
	typeOfA := reflect.TypeOf(Zero)
	// Name返回直接类型的名称,Kind返回底层类型的种类
	fmt.Println(typeOfA.Name(), typeOfA.Kind())  // myInt, int
}

指针与指针指向的元素

go语言程序中对指针获取反射对象时,可以通过reflect.Elem()方法获取这个指针指向的元素类型,这个获取过程被称为趣元素,
等效于对指针变量做了一个*操作,代码如下:

func main() {
	// 声明一个空结构体
	type cat struct{}
	// 创建cat实例
	c := &cat{}
	// 获取结构体实例的反射类型对象
	typeOfC := reflect.TypeOf(c)
	fmt.Printf("name:%s, kind:%s\n", typeOfC.Name(), typeOfC.Kind())  // name:, kind:ptr
	// 取类型的元素
	typeOfC = typeOfC.Elem()
	fmt.Printf("name:%s, kind:%s\n", typeOfC.Name(), typeOfC.Kind())  // name:cat, kind:struct
}

注意:go语言的反射中对所有指针变量的种类都是Ptr,但需要注意的是,指针变量的类型名称是空,不是*cat

使用反射获取结构体的成员类型

任意值通过reflect.TypeOf()获取到反射对象后,如果它的类型是结构体,可以通过反射对象reflect.Type的NumField()和Field()方法
获取结构体成员的详细信息
与成员获取相关的 reflect.Type 的方法如下表所示。
结构体成员访问的方法列表

方法

说明

Field(i int) StructField

根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机

NumField() int

返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机

FieldByName(name string) (StructField, bool)

根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机

FieldByIndex(index []int) StructField

多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机

FieldByNameFunc(match func(string) bool) (StructField,bool)

根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机

  1. 结构体字段类型
    reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。

StructField 的结构如下:

type StructField struct {
    Name string          // 字段名
    PkgPath string       // 字段路径
    Type      Type       // 字段反射类型对象
    Tag       StructTag  // 字段的结构体标签
    Offset    uintptr    // 字段在结构体中的相对偏移
    Index     []int      // Type.FieldByIndex中的返回的索引值
    Anonymous bool       // 是否为匿名字段
}

字段说明如下:

Name:为字段名称。
PkgPath:字段在结构体中的路径。
Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
Index:FieldByIndex 中的索引顺序。

  1. 获取成员反射信息
    下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
    反射访问结构体成员类型及信息:
func main() {
	// 定义结构体
	type cat struct {
		Name string
		Type int `json:"type" id:"100"`
	}
	// 创建结构体实例
	ins := cat{Name: "小猫", Type: 1}
	// 获取结构体实例的反射类型对象
	typeOfCat := reflect.TypeOf(ins)
	// 遍历结构体所有成员
	for i := 0; i < typeOfCat.NumField(); i++ {
		// 获取每个成员的类型信息
		fieldType := typeOfCat.Field(i)
		// 输出成员名和tag
		fmt.Printf("name:%s, tag:%s\n", fieldType.Name, fieldType.Tag)
	}
	// 通过字段名,找到字段类型信息
	if catType, ok := typeOfCat.FieldByName("Type"); ok {
		fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
	}
}

代码说明如下:

第 10 行,声明了带有两个成员的 cat 结构体。
第 13 行,Type 是 cat 的一个成员,这个成员类型后面带有一个以 ` 开始和结尾的字符串。这个字符串在Go语言中被称为 Tag(标签)。一般用于给字段添加自定义信息,方便其他模块根据信息进行不同功能的处理。
第 16 行,创建 cat 实例,并对两个字段赋值。结构体标签属于类型信息,无须且不能赋值。
第 18 行,获取实例的反射类型对象。
第 20 行,使用 reflect.Type 类型的 NumField() 方法获得一个结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。
第 22 行,reflect.Type 中的 Field() 方法和 NumField 一般都是配对使用,用来实现结构体成员的遍历操作。
第 24 行,使用 reflect.Type 的 Field() 方法返回的结构不再是 reflect.Type 而是 StructField 结构体。
第 27 行,使用 reflect.Type 的 FieldByName() 根据字段名查找结构体字段信息,catType 表示返回的结构体字段信息,类型为 StructField,ok 表示是否找到结构体字段的信息。
第 29 行中,使用 StructField 中 Tag 的 Get() 方法,根据 Tag 中的名字进行信息获取。

结构体标签(Struct Tag)

通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(StructTag)。结构体标签是对结构体字段的额外信息标签。

  1. JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
    结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。
    从结构体标签中获取值
  2. StructTag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:
    func (tag StructTag) Get(key string) string:根据 Tag 中的键获取对应的值,例如key1:"value1" key2:"value2"的 Tag 中,可以传入“key1”获得“value1”。
    func (tag StructTag) Lookup(key string) (value string, ok bool):根据 Tag 中的键,查询值是否存在。
  3. 结构体标签格式错误导致的问题
    编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,示例代码如下:
func main() {
	// 定义结构体
	type cat struct {
		Name string
		Type int `json: "type" id:"100"`
	}
	// 获取结构体实例反射类型对象
	typeOfcat := reflect.TypeOf(cat{})
	// 通过字段名获取字段类型信息
	if catType, ok := typeOfcat.FieldByName("Type"); ok {
		fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
	}
}

运行上面的代码会输出一个空字符串,并不会输出期望的 type。

代码第 11 行中,在 json: 和 “type” 之间增加了一个空格,这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。所以将第 12 行代码修改为下面的样子,则可以正常打印。

func main() {
	// 定义结构体
	type cat struct {
		Name string
		Type int `json:"type" id:"100"`
	}
	// 获取结构体实例反射类型对象
	typeOfcat := reflect.TypeOf(cat{})
	// 通过字段名获取字段类型信息
	if catType, ok := typeOfcat.FieldByName("Type"); ok {
		fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
	}
}