反射(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
下图可以帮助更进一步地理解:
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
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
-
取这个变量的地址或者这个变量所在的结构体已经是指针类型。
-
使用 reflect.ValueOf 进行值包装。
-
通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
-
使用 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 包的相关接口文档参考:
如果反射值对象(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、结果对比
结论
- 能使用原生代码时,尽量避免反射操作。
- 提前缓冲反射值对象,对性能有很大的帮助。
- 避免反射函数调用,实在需要调用时,先提前缓冲函数参数列表,并且尽量少地使用返回值