# 7. Go语言反射

# 7.1 反射基本概念与类型信息获取

反射: 在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制。

反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go语言提供了 reflect 包来访问程序的反射信息。

reflect提供了两种类型来进行访问接口变量的内容:

类型 作用
reflect.ValueOf() 获取输入参数接口中的数据的值,如果为空则返回0 <- 注意是0
reflect.TypeOf() 动态获取输入参数接口中的值的类型,如果为空则返回nil <- 注意是nil
  • reflect.Type

    在Go语言程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。

    相较于 Type 而言,Kind 所表示的范畴更大。类似于家用电器(Kind)和电视机(Type)之间的对应关系。或者电视机(Kind)和 42 寸彩色电视机(Type)。Type 是类型。Kind 是类别。Type 和 Kind 可能相同,也可能不同。通常基础数据类型的 Type 和 Kind 相同,自定义数据类型则不同。

    Type指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

    而种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

    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
        Ptr
        Slice
        String
        Struct
        UnsafePointer
    )
    

    代码示例如下:

    import (
    	"fmt"
    	"reflect"
    )
    
    // 声明一个空结构体
    type MyData struct {
    }
    
    func main() {
    	var a int //声明int类型变量a
    	typeofA := reflect.TypeOf(a) // 获取变量a的反射类型对象
    	fmt.Println(typeofA.Kind(), typeofA.Name())
    	typeofData := reflect.TypeOf(MyData{}) // 获取结构体实例的反射类型对象
    	fmt.Println(typeofData.Kind(), typeofData.Name())
    }
    

    编译运行结果:

    int int
    struct MyData
    

    如上代码, 使用 typeofA.Kind() 来获取 Kind 种类, 使用 typeofA.Name() 来获取 Type 类型 。

    获取指针指向的元素类型

    通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作,代码如下:

    import (
    	"fmt"
    	"reflect"
    )
    
    func main() {
    
    	// 声明一个空结构体
    	type St struct {
    	}
    	// 创建实例
    	s := &St{}
    	// 获取结构体实例的反射类型对象
    	typeOfs := reflect.TypeOf(s)
    	// 显示反射类型对象的名称和种类
    	fmt.Printf("name:'%v' kind:'%v'\n", typeOfs.Name(), typeOfs.Kind())
    	// 取类型的元素
    	typeOfs = typeOfs.Elem()
    	// 显示反射类型对象的名称和种类
    	fmt.Printf("element type: '%v', element kind: '%v'\n", typeOfs.Name(), typeOfs.Kind())
    }
    

    代码输出如下:

    name:'' kind:'ptr'
    element type: 'St', element kind: 'struct'
    
  • reflect.Value

    Go语言中,使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)。书写格式如下:

    value := reflect.ValueOf(rawValue)
    

    reflect.ValueOf 返回 reflect.Value 类型,包含有 rawValue 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化。reflect.Value 是一些反射操作的重要类型,如反射调用函数。

    Go语言中可以通过 reflect.Value 重新获得原始值。通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示:

    方法名 说 明
    Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
    Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
    Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
    Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
    Bool() bool 将值以 bool 类型返回
    Bytes() []bytes 将值以字节数组 []bytes 类型返回
    String() string 将值以字符串类型返回

    代码使用示例:

    import (
    	"fmt"
    "reflect"
    )
    
    func main() {
    	var a int = 100                      // 声明整型变量a并赋初值
    	valueofA := reflect.ValueOf(a)       // 获取变量a的反射值对象
    	var av1 = valueofA.Interface().(int) // 获取interface{}类型的值, 通过类型断言转换
    	var av2 = valueofA.Int()             // 获取64位的值, 强制类型转换为int类型
    	fmt.Println(av1, av2)
    }
    

    代码输出如下:

    100 100
    

# 7.2 反射与结构体

# 7.2.1 反射获取结构体成员类型

任意值通过 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) 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机

代码演示反射访问结构体成员类型及信息:

import (
	"fmt"
	"reflect"
)

//定义一个学生结构体
type NewStudent struct {
	Id    int    `json:"Id" id:"22"`
	Name  string `json:"Name" Name:"xjx"`
	Sex   bool   `json:"Sex" Sex:"true"`
	Class int    `json:"Class" Class:"1"`
}

func main() {
	// 创建NewStudent的实例
	ins := NewStudent{Id: 1, Name: "xjx", Sex: true, Class: 1}
	// 获取结构体实例的反射类型对象
	typeOfins := reflect.TypeOf(ins)

	for i := 0; i < typeOfins.NumField(); i++ {
		// 获取每个成员的结构体字段类型
		fieldType := typeOfins.Field(i)
		// 输出成员名和tag
		fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
	}

	if sType, ok := typeOfins.FieldByName("Name"); ok {
		// 从tag中取出需要的tag
		fmt.Println(sType.Tag.Get("json"), sType.Tag.Get("Name"))
	}
}

代码输出如下:

name: Id  tag: 'json:"Id" id:"22"'
name: Name  tag: 'json:"Name" Name:"xjx"'
name: Sex  tag: 'json:"Sex" Sex:"true"'
name: Class  tag: 'json:"Class" Class:"1"'
Name xjx

# 7.2.2 反射获取结构体成员值

反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示:

方 法 备 注
Field(i int) Value 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机
NumField() int 返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机
FieldByName(name string) Value 根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机
FieldByIndex(index []int) Value 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机
FieldByNameFunc(match func(string) bool) Value 根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机

代码演示反射访问结构体成员值:

import (
	"fmt"
	"reflect"
)

//定义一个学生结构体
type NewStudent struct {
	Id    int    `json:"Id" id:"22"`
	Name  string `json:"Name" Name:"xjx"`
	Sex   bool   `json:"Sex" Sex:"true"`
	Class int    `json:"Class" Class:"1"`
	p     *NewStudent
}

func main() {
	// 创建NewStudent的实例
	ins := NewStudent{Id: 1, Name: "xjx", Sex: true, Class: 1, p: &NewStudent{}}
	// 获取结构体实例的反射类型对象
	valueOfins := reflect.ValueOf(ins)

	// 获取字段数量
	fmt.Println("NumField:", valueOfins.NumField())

	// 获取索引为2的字段(bool字段)
	floatField := valueOfins.Field(2)
	// 输出字段类型以及值
	fmt.Println("Field Type:", floatField.Type(), "Field Value:", floatField.Bool())

	// 根据名字查找字段
	fmt.Println("FieldByName(\"Name\").Type", valueOfins.FieldByName("Name").Type())

	// 根据索引查找值中, p字段的索引为2的字段的类型,即为Sex字段
	fmt.Println("FieldByIndex([]int{4, 2}).Type()", valueOfins.FieldByIndex([]int{4, 2}).Type())
}

代码输出如下:

NumField: 5
Field Type: bool Field Value: true
FieldByName("Name").Type string
FieldByIndex([]int{2, 0}).Type() bool

# 7.3 反射其他操作

# 7.3.1 零值和空值判断

反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示:

方 法 说 明
IsNil() bool 返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil操作
IsValid() bool 判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。

反射值对象的零值和有效性判断示例:

import (
	"fmt"
	"reflect"
)

// 实例化一个结构体
type S struct {
	id int
}

func (this S) Hello() {
	fmt.Println(this.id)
}

func main() {

	//定义切片i
	var i []int
	fmt.Println("i IsValid():", reflect.ValueOf(i).IsValid(), "i IsNil():", reflect.ValueOf(i).IsNil())

	//定义空指针
	var p *int
	fmt.Println("p IsValid():", reflect.ValueOf(p).IsValid(), "p IsNil():", reflect.ValueOf(p).IsNil())

	// *int类型的空指针
	fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())

	// 实例化一个结构体
	si := S{id: 1}

	// 尝试从结构体中查找一个存在的字段
	fmt.Println("存在的结构体成员:", reflect.ValueOf(si).FieldByName("id").IsValid())
	fmt.Println("存在的结构体方法:", reflect.ValueOf(si).MethodByName("Hello").IsValid())

	// 尝试从结构体中查找一个不存在的字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(si).FieldByName("xxx").IsValid())
	fmt.Println("不存在的结构体方法:", reflect.ValueOf(si).MethodByName("hello2").IsValid())

	// 实例化一个map
	m := map[int]int{}
	// 尝试从map中查找一个不存在的键
	fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

编译运行结果:

i IsValid(): true i IsNil(): true
p IsValid(): true p IsNil(): true
(*int)(nil): false
存在的结构体成员: true
存在的结构体方法: true
不存在的结构体成员: false
不存在的结构体方法: false
不存在的键: false

# 7.3.2 通过反射修改值

使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。

使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表:

方法名 备 注
Elem() Value 取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value
Addr() Value 对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机
CanAddr() bool 表示值是否可寻址
CanSet() bool 返回值能否被修改。要求值可寻址且是导出的字段

使用 reflect.Value 修改值的相关方法如下表所示:

Set(x Value) 将值设置为传入的反射值对象的值
Setlnt(x int64) 使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机
SetUint(x uint64) 使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机
SetFloat(x float64) 使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机
SetBool(x bool) 使用 bool 设置值。当值的类型不是 bod 时会发生宕机
SetBytes(x []byte) 设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机
SetString(x string) 设置字符串值。当值的类型不是 string 时会发生宕机
  • 值可修改条件之一:可被寻址

    通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:

    func main() {
    	i := 2
    	valueofI := reflect.ValueOf(i)
    	valueofI.SetInt(3)
    }
    

    程序运行崩溃,打印错误:

    panic: reflect: reflect.Value.SetInt using unaddressable value
    

    报错意思是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 i 的值,而不是 i 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:

    import (
    	"fmt"
    	"reflect"
    )
    
    func main() {
    	i := 2
    	// 获取变量a的反射值对象(a的地址)
    	valueofI := reflect.ValueOf(&i)
    
    	//判断 valueofI.Elem()是否可被寻址
    	fmt.Println(valueofI.Elem().CanAddr())
    
    	//判断打印寻址地址
    	fmt.Println(valueofI.Elem().Addr())
    
    	// 取出a地址的元素(a的值)
    	valueofI = valueofI.Elem()
    	// 修改a的值为3
    	valueofI.SetInt(3)
    	fmt.Println(i, valueofI.Int())
    }
    

    运行结果:

    true
    0xc000056058
    3 3
    

    # 提示

    当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。

  • 值可修改条件之一:被导出

    结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:

    import (
    	"fmt"
    	"reflect"
    )
    
    func main() {
    	//定义结构体
    	type duck struct {
    		id int
    	}
    	// 获取duck实例地址的反射值对象
    	v := reflect.ValueOf(&duck{id: 1})
    	// 取出duck实例地址的元素
    	v = v.Elem()
    	//获取id 字段的值
    	vcount := v.FieldByName("id")
    	// 尝试设置id的值(这里会发生崩溃)
    	vcount.SetInt(5)
    	fmt.Println(vcount.Int())
    }
    

    代码输出如下:

    panic: reflect: reflect.Value.SetInt using value obtained using unexported field
    

    错误提示id该值为被导出,为了能修改这个值,需要将该字段导出。将 duck 中的 id 的成员首字母大写,导出 Id 让反射可以访问,修改后的代码如下:

    import (
    	"fmt"
    	"reflect"
    )
    
    func main() {
    	//定义结构体
    	type duck struct {
    		Id int
    	}
    	// 获取duck实例地址的反射值对象
    	v := reflect.ValueOf(&duck{Id: 1})
    	// 取出duck实例地址的元素
    	v = v.Elem()
    	//获取id 字段的值
    	vcount := v.FieldByName("Id")
    	// 尝试设置id的值(这里会发生崩溃)
    	vcount.SetInt(5)
    	fmt.Println(vcount.Int())
    }
    

    结果输出: 5

    值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:

    1. 取这个变量的地址或者这个变量所在的结构体已经是指针类型。

    2. 使用 reflect.ValueOf 进行值包装。

    3. 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。

    4. 使用 Value.Set 设置值。

# 7.3.3 通过反射创建实例

当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var a int
    // 取变量a的反射类型对象
    typeOfA := reflect.TypeOf(a)
    // 根据反射类型对象创建类型实例
    aIns := reflect.New(typeOfA)
    // 输出Value的类型和种类
    fmt.Println(aIns.Type(), aIns.Kind())
}

代码输出如下:

*int ptr

# 7.3.4 通过反射调用函数

如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。

下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。

反射调用函数:

import (
	"fmt"
	"reflect"
)

//定义 MyData结构体
type MyData struct {
	id   int
	name string
}

//定义设置ID函数
func (data *MyData) SetId(i int) {
	data.id = i
}

//定义设置name函数
func (data *MyData) SetName(name string) {
	data.name = name
}

//定义打印MyData结构体数据函数
func (data MyData) PrintData() {
	fmt.Println(data.id, data.name)
}

func main() {
	data := &MyData{id: 1, name: "test"}
	v := reflect.ValueOf(&data)
	params := []reflect.Value{reflect.ValueOf(2)}
	v.Elem().MethodByName("SetId").Call(params)
	params[0] = reflect.ValueOf("xjx")
	v.Elem().MethodByName("SetName").Call(params)
	v.Elem().MethodByName("PrintData").Call(nil)
}

运行结果:

2 xjx

# 提示

反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。

最后更新时间: 3/2/2023, 4:55:04 PM