反射?光的反射吗?在物理学中有反射这个概念,表示光在分界面上改变传播方向又返回原来物质中的现象。本文讲述的是反射是在计算机领域的概念,在计算机科学中,反射编程(reflective programming)定义如下,这是来做维基百科的描述。
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
翻译过来是说,反射编程或反射是指程序在运行时,可以检查、访问和修改它结构(内存布局)或行为的一种能力。简单来说就是在程序运行的过程中访问和修改它自己。具体来说,可执行程序是由数据+指令构成的,修改它的结构即修改数据,对应到源代码中就是修改数据结构和变量内容,修改它的行为即修改指令,对应到源代码中就是修改调用函数。
想一想,如果是让你实现这个反射功能怎么做呢?也许有读者会说,那可以这样做,要反射程序A,我直接写个程序在程序A运行的过程中直接修改他的内存数据和指令,确实可以,不过难度很高,这是黑客玩的。对于我们来说,希望有一种技术机制能够通过编程来实现,在Golang中,标准库提供了reflect包,该包提供了API让我们可以很方便进行反射编程。
也许有读者说,用接口也可以呀,确实将参数定义成接口,给接口传不同的实参,就能够动态的调用对应的函数。面向接口编程确实可以以简化编写分别适用于多种不同情形的功能代码,但是反射可以解决比面向接口编程更加普通的场景,下面举一个例子。
有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。在fmt库中有一个Sprint函数,fmt.Sprint入参是一个interface{}可变参数,返回一个string对象,它可以用来对任意类型的值进字符串输出,甚至是用户自定义的类型。下面我们也来尝试实现一个类似功能的函数,这里简化处理,只接受一个参数不支持可变参数,然后返回一个string。
type stringer interface {
String() string
}
func MySprint(a interface{}) string {
switch a := a.(type) {
case stringer:
return a.String()
case string:
return a
case int:
return strconv.Itoa(a)
case int8:
return strconv.Itoa(int(a))
case bool:
if a {
return "true"
} else {
return "false"
}
default:
return ""
}
}
如果传入的类型是 int16, int32, int64, []int, map[string]string等等类型呢?
我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何
处理类似于url.Values这样的具体的类型呢?url.Values类型定义如下:
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string
Values是基于map[string][]string创建的新类型,它和map[string][]string是不同的类型,底层的元数据信息是不同的。在
也就是说url.Values并不能匹配map[string][]string类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。
没有办法来检查未知类型了, 但是使用反射就能搞定这种情况,这是为什么需要反射的原因。
根据反射编程的定义,是在运行时能够检查、访问和修改对象的内存布局、调用方法的行为。这里的对象是泛指的概念,可以是内置类型变量,可以是自定义结构体变量,也可以是函数变量。在计算机中,
对一个对象我们可以从类型+值两个维度去描述它。对一个结构体对象来说,类型描述了结构体的名称,它有哪些字段,有哪些方法,对齐方式等等,对一个函数对象来说,类型需要描述函数名称,函数的入参信息,
函数的出参信息等。值让我们可以有途径修改它的内容,修改它的内存布局,方法地址等等。Golang的reflect包正是提供了一系列函数和方法API,可以方便的获取对象的类型和修改它的值。
这里可以读者稍做停顿,想一想,如果是我自己来设计reflect的API,我该怎么做? 根据上面的介绍,对任意一个对象,可以获取到它的类型和值描述性,所以要定义两个函数,签名如下
func typeOf(i interface{}) Type{
...
}
func valueOf(i interface{}) Value{
...
}
两个函数的入参要定义成interface{}类型,因为只有定义成interface{}类型,才可以接受任意类型的参数。反会值Type和Value分别描述类型和值信息。
下面分析reflect包,从源码的角度分析反射的实现。说明,下面的分析基于的go版本是1.14
reflect包两个核心结构是reflect.Type和reflect.Value,对应到上面就是从类型+值去描述一个对象,这两种类型都是可以导出,在外面可以使用它们,构建反射编程。
reflect.Type是一个接口类型,定义了一系列函数从各个方面去描述对象的类型信息,例如结构体的名称是什么,占用的大小是多少,有多个个字段。该接口一共定义了
31个方法,其中29个可导出类型,另外2个是不可导出,是包内使用的。在29个可导出类型中,可以分为两部分,一部分是对所有的类型都能使用的方法(下表中标注的通用),另一部是的方法是有适用范围的,只有某些甚至某个类型才能调用,不正确的类型调用将引发panic。具体类型说明下表。
函数名 | 是否可导出 | 函数适用的类型 | 说明 |
---|---|---|---|
Align | 是 | 通用 | 类型占用内存大小 |
FieldAlign | 是 | 通用 | 返回该类型作为结构体字段时占用的内存大小 |
Method | 是 | 通用 | 返回第i个方法 |
MethodByName | 是 | 通用 | 返回给定名称的方法 |
NumMethod | 是 | 通用 | 可导出方法的数量 |
Name | 是 | 通用 | 返回包内部类型的名称 |
PkgPath | 是 | 通用 | 返回定义类型的包的路径 |
Size | 是 | 通用 | 返回该类型的值占用的字节数 |
String | 是 | 通用 | 返回类型的字符串表示 |
Kind | 是 | 通用 | 返回该类型的具体类型,每种类型都有唯一的kind值 |
Implements | 是 | 通用 | 判断该类型是否实现了接口类型u |
AssignableTo | 是 | 通用 | 表示该类型的值能否赋值给类型u |
ConvertibleTo | 是 | 通用 | 表示该类型的值能否转成u类型 |
Comparable | 是 | 通用 | 表示该类型的值是否可以比较的 |
Bits | 是 | Int*,Uint*,Float*,Complex* | 以bits单位返回类型的大小 |
ChanDir | 是 | Chan | 返回channel类型的方向 |
IsVariadic | 是 | Func | 表示函数的最后一个入参是否是变参类型 |
Elem | 是 | Array,Chan,Map,Ptr,Slice | 返回一个该类型元素的Type |
Field | 是 | Struct | 返回结构体的第i个字段 |
FieldByIndex | 是 | Struct | 根据输入的下标序列返回对应的嵌套字段 |
FieldByName | 是 | Struct | 根据输入名称返回对应的字段 |
FieldByNameFunc | 是 | Struct | 根据输入的函数匹配结构体中的字段 |
In | 是 | Func | 返回第i个入参的类型 |
Key | 是 | Map | 返回Map类型的key的类型 |
Len | 是 | Array | 返回数组的长度 |
NumField | 是 | Struct | 返回结构体中字段的数量 |
NumIn | 是 | Func | 返回函数的入参数量 |
NumOut | 是 | Func | 返回函数的出参数量 |
Out | 是 | Func | 返回一个函数的第一个出参类型 |
common | 否 | - | |
uncommon | 否 | - |
reflect.Type接口定义的接口详细的功能说明如下源码注解,为什么将Type定义成接口而不是结构体,一个可解释得说法是,标准库的作者不想让使用者关注它的方法外的其他内容,Type是从各个方面描述对象的类型信息,是只读的,使用只管调用提供的方法就可以了,这体现了go语言的哲学思想,”less is more“的原则。
// Type是一个接口类型,描述了Go对象的类型信息。所有信息的获取通过Type提供的方法,
// Type涵盖了所有类型对象的信息,包括已知的类型int,map还是未知的自定义类型Mystruct
// 除了一些通用的信息,描述了类型都具有的信息,对特定的类型,例如channel, 它具有
// 方向属性,所以Type中定义的函数,有些是通用的有些是对特定类型才能使用的,
// 建议我们在调用方函数之前,可以调用Kind方法判断一下类型,以防止不合时宜的调用会
// 引发panic
// Type类型值是可以进行比较的,即可以进行==操作,所以它可以作为map的key
// Type比较基于他们代表的类型,如果代表的类型相同,两个Type的值是相等的
type Type interface {
// 返回该类型在内存中分配时占用大小,单位为字节
Align() int
// 返回该类型作为结构中的一个字段时占用的内存大小,单位为字节
FieldAlign() int
// 根据输入的下标索引返回类型定义中的第i个方法,输入下标的范围为[0,NumMethod())
// 传入的参数超过这个范围将引发panic
// 对于一个非接口类型的T或*T,返回Method的Type和Func,fields字段描述的是一个函数,这个
// 函数的第一个参数是接收者。
// 对于一个接口类型,返回的Method的Type字段表示方法的签名,没有接收者信息,Func字段为nil。
// Method返回的是可导出的方法,这些方法是按字典序排过顺序的,也就该方法是幂等的。
Method(int) Method
// MethodByName也是返回一个方法,功能与上面的Method(int) Method类型,不同的是
// 上面是根据传入的下标返回,这里是根据传入的函数名称返回与名称一致的方法,如果不存在
// 传入名称的方法,第二个bool参数返回false表示未找到。
MethodByName(string) (Method, bool)
// 返回可导出的方法数量
NumMethod() int
// 对于定义的类型,Name返回包内部类型的名称,对于非定义的类型,Name返回""
Name() string
// PkgPath返回定义类型的包的路径,也就是import导入时路径,它唯一标识了包的路径类型
// 例如"encoding/base6e", 如果是预先声明的string/error或者没有定义的 *T/struct{}/[]int,
// 或A(A是一个非定义类型的别名),PkgPath将返回""
PkgPath() string
// Size等同于unsafe.Sizeof(xx), 它返回了该类型的值占用的内存大小,单位为字节
Size() uintptr
// String返回类型的字符串表示,这个字符串可能是一个简写的形式,例如对于encoding/base64返回的是base64
// 并不能保证唯一性,也是说两个不同的类型,他们String返回的内容是一样的
// 如果是测试比较类型,可以直接比较类型Type
String() string
// Kind返回该类型具体类型,每种基础类型都有唯一的Kind值
Kind() Kind
// Implements表示该类型是否实现了接口类型u
Implements(u Type) bool
// AssignableTo表示该类型的值能否赋值给类型u
AssignableTo(u Type) bool
// ConvertibleTo表示该类型的值能否转换成u类型
ConvertibleTo(u Type) bool
// Comparable表示该类型的值是否是可以比较的
Comparable() bool
// 下面的方法是不是通用的,依赖于Kind,也就是某个方法对于特定的类型才能使用
// Bits对Int*,Uint*,Float*,Complex*才能使用
// Bits以bits单位返回类型的大小,只有对sized或unsized的Int,Uint,Float和Complex
// 调用才是正确的,否则将引发panic
Bits() int
// ChanDir返回channel类型的方向信息,是否双向的、只可发送、只可接受
// 对不是channel类型的调用该方法将panic
ChanDir() ChanDir
// IsVariadic表示函数的最后一个入参是否是变参类型,如果该类型不是一个Func类型
// 调用该方法将会panic。
// IsVariadic使用场景,对于 func(x int, y ...float64)函数 t, 调用IsVariadic会
// 返回true, t.NumIn()=2是说它有2个入参,t.In(0)表示第一个参数,它的Type为"int",
// t.In(1)表示第二个参数,他的类型为[]float64
IsVariadic() bool
// 返回一个该类型的元素的Type
// 如果该类型不是Array,Chan,Map,Ptr,Slice, 调用Elem会引发panic
Elem() Type
// Field返回结构体类型的第i个字段,如果该类型不是struct,调用会
// 引发panic, 下标i的范围为[0,NumField())
Field(i int) StructField
// FieldByIndex根据输入的下标序列返回对应的嵌套字段,相当于对每个index调用Field
// 该函数只适用用struct类型,对非struct调用会panic
FieldByIndex(index []int) StructField
// FieldByName根据输入名称返回对应的字段,如果没有找到,第二个返回参数返回false
FieldByName(name string) (StructField, bool)
// FieldByNameFunc根据输入的函数匹配结构体中的字段,match函数接收一个string入参,
// 看结构体的字段是否满足match函数,匹配顺序是先在当前层匹配字段,然后在字段是内嵌的字段中
// 查找,就是先按光度查找,然后在按深度查找,最终停止在含有一个或多个满足match函数的结构体中
// 如果在该深度上满足match函数的字段有多个,这些字段会互相取消,并且返回没有匹配,这种行为
// 反映了go在有内嵌字段的结构的情况下对名称查找的处理方式。
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 返回函数第i个入参的类型,如果调用者的类型不是函数,将引发panic.
// 如果传入的值不在[0,NumIn())范围内,也会发生panic。
In(i int) Type
// 返回map类型的key的类型,如果调用者类型不是map类型,将引发panic.
Key() Type
// 返回数组类型的长度,如果调用者的类型不是数组,会引发panic.
Len() int
// 返回结构体中字段的数量,如果调用者的类型不是结构体,会发生panic.
NumField() int
// 返回函数的入参数量,如果调用者的类型(kind)不是函数,会发生panic.
NumIn() int
// 返回函数的出参数量,如果调用者的类型(kind)不是函数,会发生panic.
NumOut() int
// Out返回一个函数类型的第i个出参类型,如果类型的Kind不是函数,会引发panic,
// 如果i不在[0,NumOut())范围内,也会发生panic.
Out(i int) Type
// 下面两个函数是不可导出类型,给包内部使用的
common() *rtype
uncommon() *uncommonType
}
reflect.Value是一个结构体,并且是可以导出的,它表示的是将一个接口反射成一个go类型值,Value结构体有很多个方法,下面会挑几个作为实例说明,需要说明的是并不是任何类型都可以
调用Value定义的所有方法,在调用方法前看下前面的说明,是哪些类型支持该方法的调用,先用Kind方法判断Value的类型,和reflect.Type一样,调用类型不匹配的方法会引发panic。
Value结构体非常简单,只有4个字段,详细说明见下面的代码注释分析。
// Value是一个结构体,表示的是将一个接口反射成一个go 类型的值
// Value结构体有很多个方法,并不是任何类型都可以调用所有的方法,每个方法可以使用的
// 类型在方法的前面都有说明,在调用特定种类的方法前,最好先用Kind方法判断Value的
// 类型,和reflect.Type一样,调用类型不匹配的方法会引发panic.
// zero Value表示没有值,它的IsValid方法会返回false,Kind方法返回Invalid,
// String方法返回"",剩下的其他方法都会产生panic.大部分函数和方法都不会返回
// invalid Value.如果确实返回了invalid value, 在文档中会明确说明特殊条件。
// Value类型的比较,比较的是他们的Interface方法,使用==比较Value并不会比较
// 他们底层表示的值。
type Value struct {
// typ holds the type of the value represented by a Value.
// typ指向Value表示的值的类型
typ *rtype
// 指向值的指针,也就是间接指向数据,如果设置了flagIndir,ptr则是指向数据的指针。
// 只有当设置了flagIndir或typ.pointers()为true时有效。
ptr unsafe.Pointer
// flag保存了value的元数据信息,最低位是标志位:
// -flagStickyRO 通过未导出的未嵌入字段获取,因此是只读的
// -flagEmbedRO 通过未导出的嵌入字段获取,因此为只读的
// -flagIndir val保存指向数据的指针
// -flagAddr v.CanAddr为true表示flagIndir
// -flagMethod v是值方法
// 接下来的5个bit表示value的Kind类型,除了方法的values外,它会重复typ.Kind
// 其余23位以上给出了方法values的方法编号,如果flag.kind()!=Func, 可以假定
// flagMethod没有设置,如果ifaceIndir(typ),可以假定设置了flagIndir.
flag
// 一个方法的value表示一个相关方法的调用,像一些方法接收者r调用r.Read
// typ+val+flag比特位描述了接收者r,但是Kind标记位表示Func(表示是一个函数),
// 并且该标志的高位给出r的类型的方法集中的方法编号。
}
Bool方法返回它的底层值,只能对v的kind为Bool的value调用,否则将引发panic.
func (v Value) Bool() bool {
v.mustBe(Bool)
return *(*bool)(v.ptr)
}
Bytes方法返回一个字节数组,这个字节数组的值是v底层的值,如果v的底层值不是一个字节切片会产生panic.
func (v Value) Bytes() []byte {
v.mustBe(Slice)
if v.typ.Elem().Kind() != Uint8 {
panic("reflect.Value.Bytes of non-byte slice")
}
// Slice is always bigger than a word; assume flagIndir.
return *(*[]byte)(v.ptr)
}
CanAddr表示v的地址是否可以通过Addr方法获取,如果v的元素是切片,可以寻址的数组,可以寻址的结构体字段,则v是可以寻址的,CanAddr会返回true. 当v是不可以寻址的,即调用CanAddr会返回false,这时调用Addr将会产生panic.
func (v Value) CanAddr() bool {
return v.flag&flagAddr != 0
}
上面从源码的角度介绍了reflect包两个重要类型,reflect.Type接口和reflect.Value方法。
下面分析这两个类型产生的方法,reflect.TypeOf()和reflect.ValueOf()
先看TypeOf的签名,它的入参是一个空接口类型,也就说任何类型都可以传给TypeOf函数,它的返回值是Type接口,Type接口在前面已详细分析过了。下面结合源码看的内部是怎么实现的。
非常简单只有2行逻辑,先将空接口i转成类型emptyInterface, 然后调用toType方法。emptyInterface是一个结构体,它的定义也在下面给出了,可以看到包含一个rtype字段和一个unsafe.Pointer字段,继续看rtype的定义,咦,有没有很熟悉?这里的rtype定义与eface里面的_type是一模一样的,确实他们就是一样的,// rtype must be kept in sync with …/runtime/type.go:/^type._type. 注释也说了rtype要与runtime/type.go中的_type保持一致。所以将空接口i转成emptyInterface完全没有问题,它们的定义都是一样的。
这里可能会有读者问,既然rtype与runtime/type.go中的_type一模一样,那为啥在reflect包中还要重新定义一遍呢?对就是不想引用runtime包,造成对runtime包的依赖,形成reflect包和runtime包的耦合,所以这里直接重新定义一个结构一样的类型。
toType只是对eface.typ做了判空操作,eface.typ即rtype类型,*rtype类型已实现了
Type的所有方法,所以eface.typ已经是一个Type了。
有一点需要注意的是,如果传递给i的是一个空接口,TypeOf将返回nil, 这时对返回值直接调用方法会引发panic。
func TypeOf(i interface{}) Type {
// 强行将interface{}类型的i转成emptyInterface
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
// 将*rtype类型的t转成Type接口类型,如果t是为空指针,返回一个Type类型的nil.
// 在gc中,唯一关心的是nil的*rtype必须转成nil Type, 在gccgo中,需要确保相同
// 类型的多个*rtype合并成一个Type
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
对空接口i来说,那任何类型都可以传给他。具体点就是有两种类型,一种是interface变量,另一种是具体的类型变量。interface变量包含空接口interface{}变量和自定义接口变量,具体类型变量包括内置类型int8,int16,自定义结构体类型变量,还有函数类型变量。
如果i是interface变量,并且被赋值了具体类型的变量,TypeOf返回的是i被赋值变量类型(具体类型)的动态类型信息,如果i没有被赋值任何具体的类型变量,返回的是接口自身的静态类型信息。如果i是具体的类型信息,返回的是具体类型信息。下面通过一个例子加深理解。
import (
"fmt"
"reflect"
)
type Animal interface {
Say() string
Walk()
}
type cat struct{}
func (c cat) Say() string {
return "喵喵喵..."
}
func (c cat) Walk() {
fmt.Println("我走起来静悄悄")
}
func main() {
var animal Animal
animal = cat{}
fmt.Println(reflect.TypeOf(animal).Name())
fmt.Println(reflect.TypeOf(animal).Kind().String())
animal2 := new(Animal)
fmt.Println(reflect.TypeOf(animal2).Elem().Name())
fmt.Println(reflect.TypeOf(animal2).Elem().Kind().String())
cat := cat{}
fmt.Println(reflect.TypeOf(cat).Name())
fmt.Println(reflect.TypeOf(cat).Kind().String())
}
第一组 animal绑定了cat对象,所以它输出的类型是被绑定对象类型cat,cat是一个结构体,所以它的kind输出为struct, 第二组animal2是一个Animal接口,反射后元素的类型是Animal,它的类型是interface, 第三组,传递的是一个具体类型,反射返回的是具体类型的cat,kind为struct.
reflect.ValueOf返回的是一个结构体对象,传入参数类型是interface{},也就是可以传入任何类型的变量,如果i是一个nil值(接口为nil当且仅当它的类型和值都为nil),ValueOf返回Value{}, 对于非nil值,它会根据i的具体值进行初始化。它的内部实现逻辑是先调用escapes确保
i逃逸到堆上,然后调用unpackEface将i转成Value对象。
unpackEface将emptyInterface 转换成 Value。实现分为3步,先将入参 interface 强转成emptyInterface,然后判断emptyInterface.typ 是否为空,如果不为空才能读取 emptyInterface.word。最后拼装 Value 数据结构中的三个字段,*rtype,unsafe.Pointer,flag。
// ValueOf返回一个新的Value, 新Value根据入参i的具体值进行初始化
// ValueOf(nil)放回Value{}
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
// 保证i逃逸到堆上
escapes(i)
// 将空接口i转成一个Value对象
return unpackEface(i)
}
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
reflection goes from interface value to reflection object
定律1:反射可以从接口值中得到反射对象
这里要从reflect包的角度去看,反射对象指reflect.Type和reflect.Value。
对应到的函数就是就是上面介绍的reflect.ValueOf函数和reflect.TypeOf函数,它们
分别将接口interface(入参)转换成reflect.Type和reflect.Value。
reflection goes from reflection object to interface value
定律2:反射可以从反射对象中获得接口的值
定律2是定律1的逆过程,从前面reflect.Value结构体定义中国可以知道它包含了类型和值的信息,所以是可以将Value转成接口值的,通过reflect.Value提供了Interface方法。
下面结合源码进行简要分析,Interface是Value的一个方法,它将Value v转成interface{},内部调用的是packEface函数,该函数将根据v.typ和v.ptr来填充interfade的_type和data字段。
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}
func valueInterface(v Value, safe bool) interface{} {
if v.flag == 0 {
panic(&ValueError{"reflect.Value.Interface", Invalid})
}
if safe && v.flag&flagRO != 0 {
// 不允许通过接口访问不可导出的value,因为它指向的地址可能是不可修改的或者方法或是函数是不被调用的
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}
if v.flag&flagMethod != 0 {
v = makeMethodValue("Interface", v)
}
if v.kind() == Interface {
if v.NumMethod() == 0 {
return *(*interface{})(v.ptr)
}
return *(*interface {
M()
})(v.ptr)
}
// 将Value v转换成interface{}
return packEface(v)
}
func packEface(v Value) interface{} {
t := v.typ
var i interface{}
e := (*emptyInterface)(unsafe.Pointer(&i))
// 填充e的各个字段,也就是i的各个字段,因为他们指向同一个地方
switch {
case ifaceIndir(t):
if v.flag&flagIndir == 0 {
panic("bad indir")
}
ptr := v.ptr
if v.flag&flagAddr != 0 {
c := unsafe_New(t)
typedmemmove(t, c, ptr)
ptr = c
}
e.word = ptr
case v.flag&flagIndir != 0:
e.word = *(*unsafe.Pointer)(v.ptr)
default:
// Value is direct, and so is the interface.
e.word = v.ptr
}
e.typ = t
return i
}
to modify a reflection object, the value must be settable
定律3:想要修改反射对象,它的值必须是可修改的。
首先理解一点反射对象代表的是反射前的变量,对反射对象的修改要能够影响到原变量,如果不能修改原变量,这个是不被允许的。Value对象提供了一系列SetXX方法来修改反射对象。
下面的代码会在运行时发生panic,会提示panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
这里给的提示信息是value是不可寻址的。在下面代码中,调用 reflect.ValueOf传进去的是一个值类型的变量,获得的Value其实是完全的值拷贝,这个Value是不能被修改的。如果传进去是一个指针,获得的Value是一个指针副本,但是这个指针指向的地址的对象是可以改变的。
func main() {
var i int = 10
v := reflect.ValueOf(i)
v.SetInt(11) //引发panic
}
// 下面这里传入的是i地址,是可以寻址的,正常运行,i的值会被修改为11
func main() {
var i int = 10
v := reflect.ValueOf(&i)
fmt.Println("type of v ", v.Type())
fmt.Println("can set of v ", v.CanSet())
e := v.Elem()
e.SetInt(11)
fmt.Println(i)
}
Type和Value相互转换
Type类型描述的是对象的类型,不含有值信息,所以Type是不能直接转换Value对象的,可以通过reflect.New(typ Type) Value 函数创建一个指向Type类型的对象,不过这个产生的对象是默认值,即该类型的零值。反射对象Value中已含有Type的信息,Value直接提供了转换函数。
func(v Value) Type() Type{
…
}
指针型Value与值Value相互转换
指针的Value转换成值 Value可以使用Indirect()和Elem()方法,值Value转换成指针的 Value采用Addr()方法,内部实现这里不在展开分析,感兴趣的读者可以看源码。
上面所有的转换关系都包含在了下面这张图中,看这个图可以串联起所有的反射知识点。
反射在框架和库中应用的比较多,例如标准库中经典的json序列化函数Marshal和Unmarshal函数,对序列化和反序列化函数,需要知道参数的全名字段,参数的类型和值,在调用它的get和set函数进行实际的操作。
在结构体的深拷贝可以采用反射实现。这里引申一个问题,深拷贝一个结构体可以有哪些实现方法?
方法一可以采用序列化,将结构体(或者说对象)进行序列化是很多语言深拷贝的常用方式,采用json序列化或gob序列化等.
方法二采用反射,c++和java语言提供了深拷贝的方法,在c++中,可以使用memcpy函数进行深拷贝,在java中实现clone方法进行。go标准库中没有提供类似的函数,可以使用reflect包实现深拷贝。
还有在一些硬编码的地方可以尝试使用reflect减少编码量,像在实现打印输出得地方,要处理各种不同类型的输入参数,可以采用reflect大大提高生产力。
但不建议大量在生产代码中大量使用反射,一个首要原因是可读性很差,还有就是学习成本比较高,面向反射的编程需要较多的高级知识,包括框架、关系映射和对象交互,以实现更通用的代码执行。还有就是将部分信息检查工作从编译期推迟到了运行期,调用方法和引用对象并非直接的地址引用,而是通过reflect包提供的一个抽象层间接访问。此举在提高了代码灵活性的同时,牺牲了一点点运行效率。在项目性能要求较高的地方,一定要慎重考虑使用反射。由于逃避了编译器的严格检查,所以一些不正确的修改会导致程序panic。
Golang提供的reflect包可以看做是对runtime中interface的应用包装,想要灵活运用反射技术,先要理解interface实现,笔者也分享了对interface的知识讲解文章,这两篇可以联合起来看,可以更好理解。