Go 反射

Go 反射

Go 反射

通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

reflect 包

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

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

在Go语言程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串;类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。

type cat struct {
}
func main(){
    typeofCat := reflect.TypeOf(cat{})
    fmt.Println(typeofCat.Name(), typeofCat.Kind())
  //cat struct

  typeofCat := reflect.TypeOf(&cat{})
    fmt.Println(typeofCat, typeofCat.Kind())
  //*main.cat ptr
}

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

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

type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

指针与指针指向的元素

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

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

类型对象 reflect.StructField reflect.Method

reflect.StructField

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

结构体字段类型

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

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 中的索引顺序。
  • Anonymous:表示该字段是否为匿名字段。
func main() {
    // 声明一个空结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
      //注意使用结构体的一些函数时,如NumFiled等无法传入指针,否则会报reflect: NumField of non-struct type
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v' type is %s,kind is %s\n", fieldType.Name, fieldType.Tag,fieldType.Type,fieldType.Type.kind)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

上述代码先用Type.NumField获取cat结构体中字段的数量,再通过typeOfHero.Field根据index获取每个字段域类型对象,并打印他们的名字,标签,以及类型和种类。也可以通过FieldByName(“Type”)找一个字段

输出结果为

name: Name  tag: '' type is string,kind is string
name: Type  tag: 'json:"type" id:"100"'  type is int,kind is int

结构体标签(Struct Tag)

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

JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

Tag 在结构体字段后方书写的格式如下:

`key1:"value1" key2:"value2"`

向之前取tag

// 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }

结果为type 100

reflect.Method

方法 说明
Method(int) Method 根据索index查找方法
NumMethod() int 获取类型中公开的方法数量
MethodByName(string) (Method,bool) 根据方法名查找方法,只能找公开的方法

获取到的Method对象描述了方法的基本信息,包括方法名,方法类型

type Method sturct{
//方法名
  Name string
//方法类型
  Type Type 
//方法对象,可以用于调用方法
  Func Value
//方法的index
  Index int
}

注意方法一定要可导出的公开方法,即开头为大写

    type user struct {
    age  int
    name string
}

func (u user) PrintName() {
    fmt.Print(u.name)
    //u.age=2
}
func (u *user) ChangeName(s string) {
    u.name = s
}

u2 := user{
name: "s",
age:  1,
}
typeofUser := reflect.TypeOf(&u2)
for i := 0; i < typeofUser.NumMethod(); i++ {
fmt.Printf("method is %s,type is %s,kind is %s", typeofUser.Method(i).Name, typeofUser.Method(i).Type, typeofUser.Method(i).Type.Kind())
}

结果

method is ChangeName,type is func(*main.user, string),kind is func
method is PrintName,type is func(*main.user),kind is func

使用反射调用接口方法

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

一般方法的反射

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

// 普通函数
func add(a, b int) int {
    return a + b
}
func main() {
    // 将函数包装为反射值对象
    funcValue := reflect.ValueOf(add)
    // 构造函数参数, 传入两个整型值
    paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
    // 反射调用函数
    retList := funcValue.Call(paramList)
    // 获取第一个返回值, 取整数值
    fmt.Println(retList[0].Int())
}

注意TypeOf和ValueOf找到方法时,接收器参数的必要性比较

使用ValueOf 的区别

    u2 := user{
        name: "s",
        age:  1,
    }
func (u user) PrintName() {
    fmt.Print(u.name)
    //u.age=2
}
func (u user) PpName() {
    fmt.Print(u.name)
    //u.age=2
}
func (u *user) ChangeName(s string) string{
    u.name = s
  return s
}

    valueOfUser := reflect.ValueOf(&u2)
    typeofUser := reflect.TypeOf(u2)
    printName := valueOfUser.MethodByName("PrintName")
    printName.Call([]reflect.Value{})//输出s
    changeName := valueOfUser.MethodByName("ChangeName")
    result := changeName.Call([]reflect.Value{reflect.ValueOf("zzz")})//注意必须为*user才可以调用这个方法
    fmt.Println("result is", result,result[0])//输出 result is [zzz],zzz,返回的是一个切片
    fmt.Println(u2.name)//zzz
    printName2, _ := typeofUser.MethodByName("PrintName")
    printName2.Func.Call([]reflect.Value{reflect.ValueOf(u2)})//zzz 可以看到确实已经被修改

通过比较,可以发现,使用TypeOf找到方法时,还需要加Func,同时第一个参数必须指定接收器,即需要调用这个函数的对象,然而ValueOf则可以不用指定接收器,它本身就是一个接收器,可以根据函数接收器是否为指针类型,来选择ValueOf()传入的值为&user还是user

同时可以用result接受返回的结果,返回的结果为一个切片

reflect.Value 反射值对象

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

value := reflect.ValueOf(rawValue)

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

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

输出

*int ptr

使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。

可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,

方法名 说 明
Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes 类型返回
String() string 将值以字符串类型返回
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    // 获取interface{}类型的值, 通过类型断言转换
    var getA int = valueOfA.Interface().(int)
    // 获取64位的值, 强制类型转换为int类型
    var getA2 int = int(valueOfA.Int())

通过反射修改变量的值

一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

有一些 reflect.Values 是可取地址的;其它一些则不可以。

x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

其中 a 对应的变量则不可取地址。因为 a 中的值仅仅是整数 2 的拷贝副本。b 中的值也同样不可取地址。c 中的值还是不可取地址,它只是一个指针 &x 的拷贝。实际上,所有通过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可取地址的。但是对于 d,它是 c 的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用 reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的 Value。

我们可以通过调用 reflect.Value 的 CanAddr 方法来判断其是否可以被取地址:

fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

每当我们通过指针间接地获取的 reflect.Value 都是可取地址的,即使开始的是一个不可取地址的 Value

例如,slice 的索引表达式 e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。

以此类推,reflect.ValueOf(e).Index(i) 对于的值也是可取地址的,即使原始的 reflect.ValueOf(e) 不支持也没有关系。

判定及获取元素的相关方法

使用 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 时会发生宕机

以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。

在已知值的类型时,应尽量使用值对应类型的反射设置值。

值可修改条件之一:可被寻址

func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    // 尝试将a修改为1(此处会发生崩溃)
    valueOfA.SetInt(1)
}

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

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

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

func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象(a的地址)
    valueOfA := reflect.ValueOf(&a)
    // 取出a地址的元素(a的值)
    valueOfA = valueOfA.Elem()
    // 修改a的值为1
    valueOfA.SetInt(1)
    // 打印a的值
    fmt.Println(valueOfA.Int())
}

输出1

下面是对代码的分析:

  • 第 5行中,将变量 a 取值后传给 reflect.ValueOf()。此时 reflect.ValueOf() 返回的 valueOfA 持有变量 a 的地址。
  • 第 7 行中,使用 reflect.Value 类型的 Elem() 方法获取 a 地址的元素,也就是 a 的值。reflect.Value 的 Elem() 方法返回的值类型也是 reflect.Value。
  • 第 9行,此时 valueOfA 表示的是 a 的值且可以寻址。使用 SetInt() 方法设置值时不再发生崩溃。
  • 第 11 行,正确打印修改的值。

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

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

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

        type dog struct {
            legCount int
    }
    // 获取dog实例的反射值对象
    valueOfDog := reflect.ValueOf(dog{})
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("legCount")
    // 尝试设置legCount的值(这里会发生崩溃)
    vLegCount.SetInt(4)

程序发生崩溃,报错:

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

报错的意思是:SetInt() 使用的值来自于一个未导出的字段

为了能修改这个值,需要将该字段导出。将 dog 中的 legCount 的成员首字母大写,导出 LegCount 让反射可以访问,修改后的代码如下:

type dog struct {
    LegCount int
}

然后根据字段名获取字段的值时,将字符串的字段首字母大写,修改后的代码如下:

vLegCount := valueOfDog.FieldByName("LegCount")

再次运行程序,发现仍然报错:

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

这个错误表示构造的 valueOfDog 这个结构体实例不能被寻址,因此其字段也不能被修改。修改代码,取结构体的指针,再通过 reflect.Value 的 Elem() 方法取到值的反射值对象。修改后的完整代码如下:

    type dog struct {
            LegCount int
    }
    // 获取dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(&dog{})
    // 取出dog实例地址的元素
    valueOfDog = valueOfDog.Elem()
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("LegCount")
    vLegCount.SetInt(4)
    fmt.Println(vLegCount.Int())

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

  1. 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
  2. 使用 reflect.ValueOf 进行值包装。
  3. 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
  4. 使用 Value.Set 设置值。

   转载规则


《Go 反射》 朱林刚 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Go 排序 Go 排序
排序对整数,浮点数,字符串切片排序对于[]int, []float, []string这种元素类型是基础类型的切片使用sort包提供的下面几个函数进行排序。 具体实现见sort.Interface的IntSlice和Float64Slice
2021-11-18
下一篇 
Go结构体 Go结构体
Go结构体Go结构体定义type user struct { age int name string } 初始化//方式1 new方式,返回指针类型*T u2 := new(user) //方式2 var的方式,返回T类型
2021-11-18
  目录