本节读书笔记对应原书第十二章。
Go语言中的变量是分为两部分的:
还有个经常容易混的,在这里也单独记一遍:值类型和引用类型。
想想这样一个场景,需要我们实现这样一个功能:测试输入参数是否实现了String
方法,如果是的话,使用该方法,那很容易想到通过switch
去枚举输入参数可能的类型,虽然可以添加更多的case
,但是类型组合是很多的,而且这种方式没办法应对未知类型。
但是如果使用反射,那么我们可以在程序编译期将变量的反射信息,字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
在Go中,我们通过反射可以访问程序中的变量,还可以通过反射机制来修改变量。
在Go语言的反射机制中,任何接口值都由是一个具体类型
和具体类型的值
两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。
作为反射包中重要的两个类型,我们先来看看是什么吧。
Type
其实是反射包中的一个接口,在这个接口中定义了一些方法,如下所示:
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
...
Implements(u Type) bool
...
}
而反射包中的Value
并不是一个接口,而是被声明成了一个结构体,请注意,这个结构体中的字段不对外暴露(小写字母开头表示不对外暴露,不可导出),为了方便访问和修改,提供了获取/写入数据的方法:
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
// Addr returns a pointer value representing the address of v.
// It panics if CanAddr() returns false.
// Addr is typically used to obtain a pointer to a struct field
// or slice element in order to call a method that requires a
// pointer receiver.
func (v Value) Addr() Value {
//... ...
}
// Bool returns v's underlying value.
// It panics if v's kind is not Bool.
func (v Value) Bool() bool {
//... ...
}
// Bytes returns v's underlying value.
// It panics if v's underlying value is not a slice of bytes.
func (v Value) Bytes() []byte {
//... ...
}
目前并不需要太在意这些类型的内部实现,简单介绍这些只是为后面的两个函数做铺垫。
在Go语言中,使用reflect.TypeOf()
函数可以获得任意值的类型对象(reflect.Type),并返回对应动态类型的reflect.Type
。
下面的例子中简单演示了如何使用reflect.TypeOf
,以第一个小例子来说,我们将值3
作为参数传入reflect.TypeOf()
中,注意在这里将会有一个隐式的接口转换操作,为啥呢,TypeOf()
为了接受各种类型的参数,设置成了interface{}
类型,所以这里会把一个具体的值转化为空接口。
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
type test int32
var pp test = 33
t := reflect.TypeOf(pp) // a reflect.Type
fmt.Println(t.String()) // "main.test"
fmt.Println(t) // "main.test"
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File
上面一共列举了三个例子:
int
,这没有什么难度;type
关键字定义了一个类型别名test
,实际上底层的类型是int32
,在这里具体的类型是test
;io.Writer
是一个接口提供了write
方法,os.Stdout
实现了该接口的所有方法,所以你可以看到声明w
变量是合法的(这有点像是Java的面向接口编程…),但是有很多类型都实现了io.Writer
接口,在这里,动态类型指的是os.Stdout
而不是静态类型io.Writer
。 到这里应该对这句话有一点点体会了:reflect.TypeOf返回的是一个动态类型的接口值,它总是返回具体的类型。
在反射中关于类型还划分为两种:类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{
}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{
title string }
var d = person{
name: "哭唧唧哇",
age: 18,
}
var e = book{
title: "123不许动"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()
都是返回空
。
在reflect
包中定义的Kind类型如下:
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 // 底层指针
)
除了TypeOf
很重要之外,ValueOf
也不容忽视。reflect.ValueOf
接受任意的interface{}
类型,并返回动态类型的接口值的具体值。
reflect.Value 也满足 fmt.Stringer 接口,但是如果Value
是个字符串,那么String
返回的是值,其他情况返回的是具体的类型。
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v.String()) // NOTE: ""
q := reflect.ValueOf("2333")
fmt.Println(q.String()) //"2333"
下面这张图展示了`Type`、`Value`、`Interface`三者之间的转化关系,简单用文字来描述下:
调用Value
的Type()
方法将返回具体类型所对应的reflect.Type
;
调用reflect.Value.Interface
方法,将会返回一个interface{}
类型,表示reflect.Value
对应类型的具体值;
v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface() // an interface{},直接打印x的结果其实也是3,只不过区别于变量i的不同之处是类型不同
i := x.(int) // an int 这里将空接口还原成最原始的状态,必须使用显式的类型转换
fmt.Printf("%d\n", i) // "3"
虽然reflect.Value
和空接口类型都可以保存值,但是空接口隐藏值对应的表达方式和公开方法,我们没法利用它去做什么事情,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 | 将值以字符串类型返回 |
func reflectValue(x interface{
}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
一个变量是一个可寻址的内存空间(前几章提到过这个概念),我们可以通过内存地址来更新变量存储的值。那么对于有一些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/b/c
中的值都是不可取地址,咦,为什么c
明明传入了一个指针,这也不可以?a
和b
变量中传入的都是整数2
的拷贝副本,所以值不可取,对于c
来说,实际上只是一个指针&x
的拷贝,所以这就解释了为什么我们说参数传递的是值拷贝。
反射中使用专有的Elem()
方法来获取指针对应的值,所以可以使用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"
看一个实际的例子:
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{
}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect包会引发panic
}
}
func reflectSetValue2(x interface{
}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
在上面的例子中,我们通过调用可取地址的reflect.Value
的reflect.Value.Set
方法进行设置值(更新值也是可以的)。reflect.Value.Set
方法在更新值之前,会分别检查反射对象是否可以被设置以及字段是否是对外公开的,方法中会调用reflect.Value.assignTo()
返回一个新的反射对象,该对象的指针会覆盖原来的反射变量,达到更新的效果。
这里无论是在reflectSetValue2
还是reflectSetValue
中,设置200值的时候,我们通过的是SetInt
,只要变量是某种类型的有符号整数就可以,如果一个引用interface{}
类型的reflect.Value
调用SetInt
会导致panic
异常,即使interface{}
变量是个整数类型也不行。实际上还有很多用于基本数据类型的Set
方法,比如SetUint/SetString
等等。
但是要注意:
reflect.Value
调用Set
方法也会导致panic
异常;int64
的值赋给int
型变量,那样会抛出panic
; 我们通过反射可以越过go语言导出规则
的限制读取结构体中没有导出的成员,但是利用反射机制是不可以修改未导出的成员!
我们之前说过有一些reflect.Value
是可以取地址的,如果一个结构体成员属于未导出的,却又可以取地址,那么这种情况下可以修改它的值吗?
当然不行…一个可取地址的reflect.Value
会记录一个结构体成员是否为未导出成员,如果是那就拒绝修改操作,刚刚提到了一个CanAddr
方法可以判断一个变量是否可以取地址,对于这种未导出成员,调用该方法并不能确定它是否可以被修改,CanSet
既可以检查是否可以取地址也可以检查是否可以被修改。
func (v Value) IsNil() bool
IsNil()
报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
func (v Value) IsValid() bool
IsValid()
返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
IsNil()
常被用于判断指针是否为空;IsValid()
常被用于判定返回值是否有效。
func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{
}{
}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{
}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}
任意值通过reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。
reflect.Type
中与获取结构体成员相关的的方法如下表所示。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
NumField() int | 返回结构体成员字段数量。 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第i个方法 |
MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 |
写到这里,反射这一章的内容就要接近尾声了。在了解很多反射小知识之后,再去理解Go官方博客中的反射三大定律就会容易很多:
1.Reflection goes from interface value to reflection object.
翻译:
interface{}
变量可以转化成反射对象;2.Reflection goes from reflection object to interface value.
翻译:从反射对象可以获取
interface{}
变量;3.To modify a reflection object, the value must be settable.
翻译:要修改反射对象,那么它的值必须是可设置的。
如果你仔细看上面我写的内容,会发现不管是在介绍ValueOf
还是TypeOf
函数的时候,都提到了类型隐式转换为interface{}
类型。reflect.ValueOf(x)
中(假设x:=2
)的x
作为函数的入参,函数调用时会转换成空接口类型,在TypeOf
和ValueOf
函数内部,完成了空接口类型转化为reflect.Value
类型或者是reflect.Type
类型的工作,这就是第一条定律要表达的内容。
通过这两个函数就可以将反射类型和Go的数据类型关联起来。
第二条定律也很好理解。在介绍ValueOf
的时候,通过一张图梳理了以下各部分之间的转化关系。
reflect.Value
类型的Interface
方法就可以实现从反射对象到interface{}
之间的转化,但是这个转化获得的变量类型是空接口类型,如果想要还原成最原始的状态,那么需要进行显示转化(上面也有例子噢)。
但是不管从接口值到反射对象,还是从反射对象到接口值,都会经历两次转换,不过需要说明的是,如果变量本身就是空接口类型,那就不需要类型转化了(虽然这个过程是隐式的):
前两条还比较好理解,通过TypeOf
和ValueOf
函数可以实现第一条,实现第二条就可以用前面提到的Value
类型结构体中的interface()
方法实现逆转化。
第三条耗尽洪荒之力可能也不太好理解了,典型明明英文读的懂就是不知道它在说啥。官方啥意思呢,先翻译一下就是你要是想操作反射变量,那么这个值必须是可设置的。啥叫可设置的呢,我们原来有个变量A(就是原变量),然后通过反射拿到了它对应的反射变量B,我们对变量B的修改同时会影响原变量A,那么这就属于可设置的。
如果使用反射的时候,不清楚这一点,直接取修改反射对象,那么就要恭喜你喜提panic
一次了。
func main() {
i := 1
v := reflect.ValueOf(i)
v.SetInt(10)
fmt.Println(i)
}
输出:
[root@cdb63e69049f ~/go/src/MathDemoServer/MathDemoServer]# go run main.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82)
/usr/local/go/src/reflect/value.go:247 +0x138
reflect.flag.mustBeAssignable(...)
/usr/local/go/src/reflect/value.go:234
reflect.Value.SetInt(0x49e860, 0xc000016088, 0x82, 0xa)
/usr/local/go/src/reflect/value.go:1606 +0x3b
main.main()
/root/go/src/MathDemoServer/MathDemoServer/main.go:11 +0xb2
exit status 2
所以咋整呢,只需要三步:首先获取变量指针,其次获取指针指向的变量,最后更新变量的值。这三步分别对应三个函数:
调用 reflect.ValueOf
函数获取变量指针;
调用 reflect.Value.Elem
方法获取指针指向的变量;
调用 reflect.Value.SetInt
方法更新变量的值。
上面的内容列举了例子啊,如果忘记了就往上翻翻。
在一些博客中看到这样一段话(大意是反射效率不高),照着Golang反射性能分析这篇博客简单测了一下,确实反射挺慢…
反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
这部分就有点难理解了…所以掉了好多头发…现在不太理解也没事,跳过去就好了,会看懂的。
Go 语言的 interface{}
类型在语言内部是通过 emptyInterface
这个结构体来表示的,其中的 rtype
字段用于表示变量的类型,另一个 word
字段指向内部封装的数据:
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
用于获取变量类型的 reflect.TypeOf
函数将传入的变量隐式转换成空接口interface{}
类型,然后通过底层编程的一些方法转化成emptyInterface
类型,赋值给eface
, 最后返回其中存储的类型信息 rtype
:
func TypeOf(i interface{
}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
rtype
就是一个实现了 Type
接口的结构体,我们能在 reflect
包中找到reflect.rtype.String
方法帮助我们获取当前类型的名称等信息:
func (t *rtype) String() string {
s := t.nameOff(t.str).name()
if t.tflag&tflagExtraStar != 0 {
return s[1:]
}
return s
}
reflect.TypeOf
函数的实现原理其实并不复杂,它只是将一个 interface{}
变量转换成了内部的 emptyInterface
表示,然后从中获取相应的类型信息。
用于获取接口值 Value
的函数 reflect.ValueOf
实现也非常简单。reflect.unpackEface
函数会将传入的接口转换成 emptyInterface
结构体,然后将具体类型和指针包装成 Value
结构体并返回:
func ValueOf(i interface{
}) Value {
if i == nil {
return Value{
}
}
//......
return unpackEface(i)
}
func unpackEface(i interface{
}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
t := e.typ
if t == nil {
return Value{
}
}
f := flag(t.Kind())
//... ...
return Value{
t, e.word, f}
}
当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换的工作,将变量的类型和值转换成了 interface{}
并等待运行期间使用 reflect
包获取接口中存储的信息。
虽然在大多数的应用和服务中并不常见到使用反射(可能是因为大量使用反射代码比较难懂、以及性能比较差吧),但是很多框架都依赖反射机制去实现简化代码逻辑的工作(比如开源的gorm
框架就用到了反射去处理数据库表字段和表结构体的一一对应的关系)。
reflect
包中最重要的2个函数就是reflect.TypeOf
和reflect.ValueOf
,前者完成了获取类型信息的工作,知道了变量的类型之后,我们可以获得类型实现的方法,获取类型包含的全部字段,对于不同的数据结构,可以获得的信息不同:
结构体:获取字段的数量并通过下标和字段名获取字段 StructField
;
哈希表:获取哈希表的 Key
类型;
函数或方法:获取入参和返回值的类型;
reflect.ValueOf
获取动态类型具体的值。此外反射包还提供了两个类型Type
和Value
,和刚刚提到的函数是一一对应的关系。
当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换的工作,将变量的类型和值转换成了 interface{}
并等待运行期间使用 reflect
包获取接口中存储的信息。