Go语言反射(reflect)

反射(reflection)是在 Java 出现后流行起来的,通过反射可以在运行时获取丰富的类型信息以及更新操作,支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期进行获取,并且有能力修改它们。但是反射带来的问题是代码的可读性差和性能问题,下文会讲到。

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

反射的类型(Type)与种类(Kind)

        在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。、

 1、反射种类(Kind)的定义

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

种类(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        // 底层指针
)

Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。

2、反射类型对象(Type)

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

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

package main

import (
    "fmt"
    "reflect"
)

// 定义一个Enum类型
type Enum int

const (
    Zero Enum = 0
)

func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(cat{})
    // 显示反射类型对象的名称和种类  => cat struct
    fmt.Println(typeOfCat.Name(), typeOfCat.Kind())
    // 获取Zero常量的反射类型对象
    typeOfA := reflect.TypeOf(Zero)
    // 显示反射类型对象的名称和种类 => Enum int
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

运行结果如下:

cat struct
Enum int

reflect.TypeOf 方法的底层相关实现参考:Go语言 interface 理解 & 与反射的关系_judgejames的博客-CSDN博客理解 interface1、interface 是一种类型准确来说,interface 是带有一组方法的一种类型,这些方法定义了 interface 的行为。如果一个类型实现了一个 interface 中所有方法,则该类型实现了该 interface。又因为 go 允许不带任何方法的interface存在,这种interface成为空interface。所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interfahttps://blog.csdn.net/judgejames/article/details/124400156?spm=1001.2014.3001.5501

下图可以帮助更进一步地理解:

 2.1 判断类型是否实现了指定的接口

type I interface {
    GetA() int
    SetA(a int)
}

type S struct {
    A int
}

func(s S) GetA() int {
    return s.A
}

func(s *S) SetA(a int) {
    s.A = a
}

func(s S) Print() {
    fmt.Println(s.A)
}

func NilOrNot(v interface{}) bool {
    fmt.Println(reflect.TypeOf(v))
    fmt.Println(reflect.TypeOf(v).Kind())
    return v == nil
}

func TestReflect(t *testing.T) {
    typeOfI := reflect.TypeOf((*I)(nil)).Elem()
    fmt.Printf("typeOfI kind is Interface %t\n", typeOfI.Kind() == reflect.Interface)

    typeOfS := reflect.TypeOf(S{})
    typeOfPS := reflect.TypeOf(&S{})

    fmt.Printf("typeOfS implements I interface %t\n", typeOfS.Implements(typeOfI))
    fmt.Printf("typeOfPS implements I interface %t\n", typeOfPS.Implements(typeOfI))
}

通过 reflect.TypeOf((*<interface>)(nil)).Elem() 获得接口类型。因为接口不可以创建实例,所以把 nil 强制转为 *I 类型。

注意:如果值类型实现了接口,则指针类型也实现了接口;反之不成立

2.2 Go语言通过类型信息创建实例

当已知 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)
    
    // 输出 aIns 的类型和种类
    fmt.Println(aIns.Type(), aIns.Kind())
}

代码输出如下:

*int ptr

代码说明:

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

使用反射读取、设置、创建

1、反射值对象

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

value := reflect.ValueOf(rawValue)

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

1.1 从反射值对象获取被包装的值

Go语言中可以通过 reflect.Value 重新获得原始值。

1.1.1 从反射值对象(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将值以字符串类型返回

1.1.2 从反射值对象(reflect.Value)中获取值的例子

下面代码中,将整型变量中的值使用 reflect.ValueOf 获取反射值对象(reflect.Value)。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。

package main

import (
    "fmt"
    "reflect"
)
     
func main() {
    
    // 声明整型变量a并赋初值
    var a int = 1024
    
    // 获取变量 a 的反射值对象,类型为 reflect.Value,这个过程和 reflect.TypeOf() 类似
    valueOfA := reflect.ValueOf(a)
    
    // 获取interface{}类型的值, 通过类型断言转换
    var getA int = valueOfA.Interface().(int)
    
    // 获取64位的值, 强制类型转换为int类型
    var getA2 int = int(valueOfA.Int())
    fmt.Println(getA, getA2)
}

 代码输出如下:

1024 1024

代码说明如下:

  • 第 17 行,将 valueOfA 反射值对象以 interface{} 类型取出,通过类型断言转换为 int 类型并赋值给 getA。

  • 第 20 行,将 valueOfA 反射值对象通过 Int 方法,以 int64 类型取出,通过强制类型转换,转换为原本的 int 类型。

1.2 判断反射值的空和有效性

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

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

下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // *int的空指针
    var a *int
    fmt.Println("var a *int:", reflect.ValueOf(a).IsNil())
    // nil值
    fmt.Println("nil:", reflect.ValueOf(nil).IsValid())
    // *int类型的空指针
    fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())
    // 实例化一个结构体
    s := struct{}{}
    // 尝试从结构体中查找一个不存在的字段
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid())
    // 尝试从结构体中查找一个不存在的方法
    fmt.Println("不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid())
    // 实例化一个map
    m := map[int]int{}
    // 尝试从map中查找一个不存在的键
    fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

代码输出如下:

var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的结构体方法: false
不存在的键: false

2、获取指针与指针指向的元素

Go语言程序中对指针获取反射对象时,可以通过 reflect.TypeOf(v).Elem() 方法获取指针指向的元素的类型,reflect.TypeOf(v).Elem() 也可以确定 Map、slice、channel、Array 包含的类型。

而reflect.ValueOf(v).Elem() 是取值的过程,等效于对指针类型变量做了一个*操作,其反射对象的 Kind 类型必须是 ptr 或 Interface 类型,否则会 panic,示例代码如下:

package main

import (
    "fmt"
    "reflect"
)

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())
    
    valueOfCat := reflect.ValueOf(ins)
    fmt.Printf(valueOfCat.String(), " ", valueOfCat.Elem())
}

运行结果如下:

name:'' kind:'ptr'
element name: 'cat', element kind: 'struct'
<*main.cat Value> {10}

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

任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象 reflect.Type 的 NumField() 和 Field() 方法获得结构体成员的详细信息。

// structType represents a struct type.
type structType struct {
   rtype
   pkgPath name
   fields  []structField // sorted by offset
}

reflect.Type 中与 struct 相关的方法如下表所示。

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

3.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 中的索引顺序。

  • Anonymous:表示该字段是否为匿名字段。

3.2 获取成员反射信息

下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。

demo1 反射访问结构体成员名 name 及 tag 信息:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

代码输出如下:

name: Name tag: ''
name: Type tag: 'json:"type" id:"100"'
type 100

demo2 使用 FieldByIndex 访问子结构体:

package main
import (
    "fmt"
    "reflect"
)
// 定义结构体
type dummy struct {
    a int
    b string
    // 嵌入字段
    float32
    bool
    next *dummy
}
func main() {
    // 值包装结构体
    d := reflect.ValueOf(dummy{
            next: &dummy{},
    })
    // 获取字段数量
    fmt.Println("NumField", d.NumField())
    // 获取索引为2的字段(float32字段)
    floatField := d.Field(2)
    // 输出字段类型
    fmt.Println("Field", floatField.Type())
    // 根据名字查找字段
    fmt.Println("FieldByName(\"b\").Type", d.FieldByName("b").Type())
    // 根据索引查找值中, next字段的int字段的值
    fmt.Println("FieldByIndex([]int{4, 0}).Type()", d.FieldByIndex([]int{4, 0}).Type())
}

代码输出如下:

NumField 5
Field float32
FieldByName("b").Type string
FieldByIndex([]int{4, 0}).Type() int

代码说明如下:

  • 第 29行,[]int{4,0} 中的 4 表示,在 dummy 结构中索引值为 4 的成员,也就是 next。next 的类型为 dummy,也是一个结构体,因此使用 []int{4,0} 中的 0 继续在 next 值的基础上索引,结构为 dummy 中索引值为 0 的 a 字段,类型为 int。

4、结构体标签(Struct Tag)

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

JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

4.1 常用的结构体标签键

  • json: 由 encoding/json 包使用,详见json.Marshal()的使用方法和实现逻辑。

  • xml : 由 encoding/xml 包使用,详见xml.Marshal()。
  • bson: 由 gobson 包,和mongo-go包使用。
  • protobuf: 由 github.com/golang/protobuf/proto 使用,在包文档中有详细说明。
  • yaml: 由 gopkg.in/yaml.v2 包使用,详见yaml.Marshal()。
  • gorm: 由 gorm.io/gorm 包使用,示例可以在GORM的文档中找到。

官方Wiki:Well known struct tags · golang/go Wiki · GitHub

4.2 结构体标签的格式

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

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

结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。

4.3 从结构体标签中获取值

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 中的键,查询值是否存在。

4.4 结构体标签格式错误导致的问题

编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,示例代码如下:

package main
import (
    "fmt"
    "reflect"
)
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"))
    }
}

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

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

所以将第 12 行代码修改为下面的样子,则可以正常打印:

type cat struct {
    Name string
    Type int `json:"type" id:"100"`
}

运行结果如下:

type

4.5 序列化 Demo

func SerializeStructStrings(s interface{}) (string, error) {
    result := ""
    
    r := reflect.TypeOf(s)
    value := reflect.ValueOf(s)

    if r.Kind() == reflect.Ptr {
        r = r.Elem()
        value = value.Elem()
    }
    if r.Kind() != reflect.Struct {
        return "", errors.New("param invaild")
    }

    for i := 0; i < r.NumField(); i++ {
        field := r.Field(i)
        key := field.Name
        if val, ok := field.Tag.Lookup("json"); ok {
            if val == "-" {
                continue
            }
            key = val
        }
        
        switch value.Field(i).Kind() {
        case reflect.String:
            result += key + ":" + value.Field(i).String() + ";"
        default:
            continue
        }
    }
    return result, nil
}

5、设置 & 修改

5.1 判断反射值对象 value 是否可修改

Go语言中类似 x、x.f[1] 和 *p 形式的表达式都可以表示变量,但是其它如 x + 1 和 f(2) 则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

对于 reflect.Value 也有类似的区别。有一些 reflect.Value 是可取地址的;其它一些则不可以。考虑以下的声明语句

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)

我们可以通过调用 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表达式不支持也没有关系。

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

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

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

reflect.Indirect(v)

func Indirect(v Value) Value

返回持有v持有的指针指向的值的Value。如果v持有nil指针,会返回Value零值;如果v不持有指针,会返回v。

5.3 值修改相关方法

使用 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 仍然修改值时会发生宕机。

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

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

package main
import (
    "reflect"
)
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 当然是不能被寻址的。将代码修改一下,重新运行:

package main
import (
    "fmt"
    "reflect"
)
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

提示

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

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

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

package main
import (
    "reflect"
)
func main() {
    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

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

package main
import (
    "reflect"
    "fmt"
)
func main() {
    type dog struct {
            LegCount int
    }
    // 获取dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(&dog{})
    // 取出dog实例地址的元素
    valueOfDog = valueOfDog.Elem()
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("LegCount")
    // 尝试设置legCount的值(这里会发生崩溃)
    vLegCount.SetInt(4)
    fmt.Println(vLegCount.Int())
}

代码输出如下:

4

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

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

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

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

  4. 使用 Value.Set 设置值。

5.4 结构体成员变量的值修改

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

下面是一个解析结构体变量 t 的例子,用结构体的地址创建反射变量,再修改它。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem() // 结构体指针
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }
}

运行结果如下:

0: A int = 23
1: B string = skidoo

T 中字段名之所以大写,是因为结构体中只有可导出的字段是“可设置”的。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)
}

运行结果如下:

t is now {77 Sunset Strip}

如果我们修改了程序让 s 由 t(而不是 &t)创建,程序就会在调用 SetInt 和 SetString 的地方失败,因为 t 的字段是不可设置的。

6、创建

Go 反射包中提供了以下几种类型的创建:

 6.1 创建slice、map、chan

func TestMake(t *testing.T) {
   // MakeSlice
   s := make([]int, 0)
   sType := reflect.TypeOf(s)
   refS := reflect.MakeSlice(sType, 0, 0)
   v := 10
   refS = reflect.Append(refS, reflect.ValueOf(v))
   fmt.Println(refS.Interface().([]int))

   // MakeMap
   m := make(map[string]int)
   refM := reflect.MakeMap(reflect.TypeOf(m))
   k := "str"
   refM.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v))
   fmt.Println(refM.Interface().(map[string]int))

   // MakeChan
   c := make(chan int)
   cType := reflect.TypeOf(c)
   refC := reflect.MakeChan(cType, 0)
   go func() {
      refC.Send(reflect.ValueOf(v))
   }()
   if val, ok := refC.Recv(); ok {
      fmt.Println(val.Interface().(int))
   }
}

运行结果:

=== RUN TestMake
[10]
map[str:10]
10

6.2 创建函数 

var swap = func(in []reflect.Value) []reflect.Value {
   return []reflect.Value{in[1], in[0]}
}

var makeSwap = func(fptr interface{}) {
   fn := reflect.ValueOf(fptr).Elem()

   v := reflect.MakeFunc(fn.Type(), swap)

   fn.Set(v)
}

func TestMakeFunc(t *testing.T)  {
   var intSwap func(int, int) (int, int)
   makeSwap(&intSwap)
   fmt.Println(intSwap(0, 1))

   var floatSwap func(float64, float64) (float64, float64)
   makeSwap(&floatSwap)
   fmt.Println(floatSwap(2.72, 3.14))
}

reflect 包的相关接口文档参考:

Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国Go语言文档中文版,Go语言中文网,中国 Golang 社区,Go语言学习园地,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。分享 Go 语言知识,交流使用经验https://studygolang.com/pkgdocGo语言通过反射调用函数

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

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

package main

import (
    "fmt"
    "reflect"
)

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(int(retList[0].Int()))
}

代码说明如下:

  • 第 14 行,将 add 函数包装为反射值对象。

  • 第 17 行,将 10 和 20 两个整型值使用 reflect.ValueOf 包装为 reflect.Value,再将反射值对象的切片 []reflect.Value 作为函数的参数。

  • 第 20 行,使用 funcValue 函数值对象的 Call() 方法,传入参数列表 paramList 调用 add() 函数。

  • 第 23 行,调用成功后,通过 retList[0] 取返回值的第一个参数,使用 Int 取返回值的整数值。

提示

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

反射三定律

反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

我们调用 reflect.TypeOf(x) 时,x 被存储在一个空接口变量中被传递过去,然后 reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息。

类型 reflect.Value 有一个方法 Type(),它会返回一个 reflect.Type 类型的对象。

Type 和 Value 都有一个名为 Kind 的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice 等。

Value 类型也有一些类似于 Int、Float 的方法,用来提取底层的数据:

  • Int 方法用来提取 int64

  • Float 方法用来提取 float64,示例代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
    fmt.Println("value:", v.Float())
}

运行结果如下:

type: float64
kind is float64: true
value: 3.4

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x uint8 = 'x'
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())                            // uint8.
    fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
    x = uint8(v.Uint())                                       // v.Uint returns a uint64.
}

运行结果如下:

type: uint8
kind is uint8: true

其次,反射对象的 Kind 方法描述的是基础类型,而不是静态类型。如果一个反射对象包含了用户定义类型的值,如下所示:

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

上面的代码中,虽然变量 v 的静态类型是 MyInt,而不是 int,但 Kind 方法仍然会返回 reflect.Int。换句话说 Kind 方法不会像 Type 方法一样区分 MyInt 和 int。

还有一些用来修改数据的方法,比如 SetInt、SetFloat。在介绍它们之前,我们要先理解“可修改性”(settability),这一特性会在下面进行详细说明。

反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”

根据一个 reflect.Value 的反射值对象,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。

其函数声明如下:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

然后,我们可以通过断言,恢复底层的具体值:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。

反射第三定律:如果要修改“反射值对象”其值必须是“可写的”

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic

如果运行这段代码,它会抛出异常:

panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

因为变量 v 是“不可写的”,“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。

我们可以通过 CanSet 方法检查一个 reflect.Value 对象的“可写性”,如:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("settability of v:", v.CanSet())
}

运行结果如下:

settability of v: false

与函数的传参一样:

f(x)
f(&x)

反射的工作机制与此相同,如果想通过反射修改变量 x,就要把想要修改的变量的指针传递给反射库。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    p := reflect.ValueOf(&x) // Note: take the address of x.
    fmt.Println("type of p:", p.Type())
    fmt.Println("settability of p:", p.CanSet())
    fmt.Println("settability of p elem:", p.Elem().CanSet())
}

运行结果如下:

type of p: *float64
settability of p: false
settability of p elem: true

反射对象 p 是不可写的,因为它是指针类型。需要通过解引用来修改它指向的值。可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”。

总结

反射规则可以总结为如下几条:

  • 反射可以将“接口类型变量”转换为“反射类型对象”;

  • 反射可以将“反射类型对象”转换为“接口类型变量”;

  • 如果要修改“反射类型对象”,其值必须是“可写的”。

Go语言反射的性能测试

1、使用反射赋值

/**
   Var Assign
*/
type A struct {
   Data int
}

// native
func BenchmarkNativeAssign(b *testing.B) {
   a := &A{
      Data: 10,
   }

   // 启动计时器
   b.ResetTimer()
   b.StartTimer()
   for i:= 0; i < b.N; i++ {
      a.Data = 9
   }
}

// reflect1
func BenchmarkReflectAssign(b *testing.B) {
   a := &A{
      Data: 10,
   }

   r := reflect.ValueOf(a).Elem()
   data := r.FieldByName("Data")

   b.ResetTimer()
   b.StartTimer()
   for i:= 0; i < b.N; i++ {
      data.SetInt(9)
   }
}
/*
func (v Value) SetInt(x int64) {
   v.mustBeAssignable()
   switch k := v.kind(); k {
   default:
       panic(&ValueError{"reflect.Value.SetInt", v.kind()})
   case Int:
       *(*int)(v.ptr) = int(x)
   case Int8:
       *(*int8)(v.ptr) = int8(x)
   case Int16:
       *(*int16)(v.ptr) = int16(x)
   case Int32:
       *(*int32)(v.ptr) = int32(x)
   case Int64:
       *(*int64)(v.ptr) = x
   }
}
*/

// reflect2
func BenchmarkReflectFindAndAssign(b *testing.B) {
   a := &A{
      Data: 10,
   }

   r := reflect.ValueOf(a).Elem()

   b.ResetTimer()
   b.StartTimer()
   for i:= 0; i < b.N; i++ {
      r.FieldByName("Data").SetInt(9)
   }
}
/*
// 通过名字查询类型对象
func (v Value) FieldByName(name string) Value {
    v.mustBe(Struct)
    if f, ok := v.typ.FieldByName(name); ok {
        return v.FieldByIndex(f.Index)
    }
    return Value{}
}
*/

2、使用反射调用函数

/**
   Function Call
*/
func foo(v int) {
}

// Func Call By Native
func BenchmarkNativeCall(b *testing.B) {

   b.ResetTimer()
   b.StartTimer()
   for i := 0; i < b.N; i++ {
      // 原生函数调用
      foo(666)
   }
}

// Func Call By Reflect
func BenchmarkReflectCall(b *testing.B) {

   f := reflect.ValueOf(foo)

   b.ResetTimer()
   b.StartTimer()
   for i := 0; i < b.N; i++ {
      f.Call([]reflect.Value{reflect.ValueOf(666)})
   }
}
/**
   反射函数调用的参数构造过程非常复杂,构建很多对象会造成很大的内存回收负担。
   Call() 方法内部就更为复杂,需要将参数列表的每个值从 reflect.Value 类型转换为内存。
   调用完毕后,还要将函数返回值重新转换为 reflect.Value 类型返回。
   因此,反射调用函数的性能堪忧。
*/

3、结果对比

结论

  • 能使用原生代码时,尽量避免反射操作。
  • 提前缓冲反射值对象,对性能有很大的帮助。
  • 避免反射函数调用,实在需要调用时,先提前缓冲函数参数列表,并且尽量少地使用返回值
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值