反射可以在程序运行的时候检查代码的元信息,动态的修改代码行为。
官网地址
开始之前,得先说说go中的类型。
type UserType int
var admin UserType
var visitor int
admin
和visitor
本质类型是一样的,但他俩的的静态类型不一样,如果不强转的话,两者是不能直接转换的。
// 定义接口变量
var r io.Reader
// 创建实际对象
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
// 接口变量存储实际的值
r = tty
r变量包含了一对数据,(tty,*os.File),通过接口变量可以访问该接口里面的方法,要是将接口变量变为实际类型,需要通过断言。在我们的例子中,代码如下:
f := r.(*os.File)
*os.File
实现了除io.Reader
中的其他接口的方法,需要通过断言来做,不能直接通过io.Reader
接口来做,因为io.Reader
只提供它的方法的访问。代码如下:
var w io.Writer
w = r.(io.Writer)
按照我们上面的所说的,上面的断言是可以成功的,因为r里面存储的是一对(实际的值,实际的类型信息),因为*os.File
确实实现了io.Writer
接口。
*os.File
实现了io.Reader
接口,io.Reader
接口中只有一个方法,*os.File
中有除io.Reader
之外的其他方法,io.Reader
是可以接收*os.File
变量的。所以类比到func TestAction(t *testing.T) {
insert := InsertAction{}
update := UpdateAction{}
CallAction(insert)
CallAction(update)
}
func CallAction(action Action) {
action.DoAction()
}
type Action interface {
DoAction()
}
type InsertAction struct {}
func (i InsertAction) DoAction() {
fmt.Println("InsertAction")
}
type UpdateAction struct {}
func (i UpdateAction) DoAction() {
fmt.Println("UpdateAction")
}
func (i UpdateAction) DoAction1() {
fmt.Println("UpdateAction")
}
同样的道理,如果把上面例子中的CallAction
中的入参变为interface{}
的话,也没有问题,但interface{}
中没有方法,所以,没有方法可以调。除非利用断言。
有了上面基础,反射只是检查存储在接口变量中的那一对数据(实际的值,实际的类型信息)。同样在反射中对应两种类型的变量
分别获取实际的值,实际的类型信息。对应两种方法来获取,从Value对象是可以拿到Type对象的。
要注意,在TypeOf,ValueOf
的入参都是interface{}(空接口),反射就通过这个接口变量来检查实际存储的值。
反射包提供了很多的方法用来操作。官方地址如下:
https://pkg.go.dev/reflect
在这里只是说明一下简单的使用,具体的还得去翻官网API,在最后会有一个demo来使用一下反射。下面从几个方面来介绍一下反射。
上面已经说了Type和Value
,通过TypeOf,ValueOf
来获取对应的值,它俩分别对应接口变量中存储的一对数据(实际值,实际类型),其中一个方法比较有意思。kind()
方法在Value和Type对象中都有,Kind方法返回Kind类型,Kind类型是int的别名,它的作用是返回变量实际的类型(int,Float64等),怎么理解实际类型呢?
type MyType int
这里的int
就是实际类型,MyType
就是静态类型。
也就是Kind是不能区分静态类型是实际类型的,但Type可以。
可以通过Value
对象来访问变量值,Value提供了一些方法可以方法到,Type
对象不行,要记住Type获取的是实际值的类型,类型是不能访问到实际的值的。代码如下:
var i MyKind = 1
ofValue := reflect.ValueOf(i)
// 对int类型的访问,如果在int类型上调用String,或者Bool这种不属于它类型的就会报错,但interface是可以的。
// 这里要注意,go是按照最高位数返回,返回类型为int64
println(ofValue.Int())
println(ofValue.String())
println(ofValue.Bool())
// 返回类型为float64
println(ofValue.Float())
// 返回类型为uint64
println(ofValue.Uint())
println(ofValue.Interface())
因为interface{}可以接受任何值,那么上面的例子中还可以这么写
func main() {
var i MyKind = 1
ofValue := reflect.ValueOf(i)
// 这里返回是实际的值,int()表示实际的类型
println(ofValue.Int())
// 返回interface利用断言来做,但是得注意,这里断言的是静态类型,如果换为int,就会报错
println(ofValue.Interface().(MyKind))
}
实际的参数不变,只是接受参数的变量类型不变,既然可以利用反射从接口变量中获取实际的值,当然也可以反着来。interface()
方法实际就是将实际的值和实际类型打包放在一块,返回了回来,在断言的时候只要保证类型一致就可以直接转换。总之interface()
方法是ValueOf
的逆方法,它的返回值也是Interface{}
。
代码如下:
var x float64 = 3.4
v := reflect.ValueOf(x)
// 会报错,报错为 panic: reflect.Value.SetFloat using unaddressable value
v.SetFloat(7.1)
Value
对象中有CanSet()
方法表示是否可以设置值,不能设置值任然设置,就会如上面的例子一样。上面报错的原因是在于v不可设置值。
ar x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet()) //结果为true
可设置值比较像可寻址,意思就是,通过变量能找到原始的值,这个属性是由反射对象所持有得原始数据项来决定的。go参数传递都是值传递
,所以当做如下的操作时:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
其实就是将x 复制了一份给ValueOf,所以,这里反射对象操作的是复制的那份数据,而不是x本身,所以,当SetFloat
的时候,就会产生歧义,作为使用者来说,想改的是x,结果x没有变。这就有问题了,可设置性就是为了处理这个问题的。如果想修改要怎么办?
传递指针。虽然指针也是copy了一份,但是两个指针指向的数据是同一个。这就消除了这种歧义。
有两个注意点:
第一个上面已经说清楚了,第二个,都已经是指针对象了为啥还不能设置值。
指针获取的反射对象是不能设置值的,因为想要设置值的不是它,他也是复制的,想要设置的是指针所指向的变量。那么?怎么获取呢?
// 获取指针对象所指向的变量
v := p.Elem()
// true
fmt.Println("settability of v:", v.CanSet())
现在v是一个可以设置值的对象,v就是上面例子中的x,所以现在可以通过v来设置x,因为它代表x。代码如下:
func main() {
var x float64 = 3.4
// 传递的是指针
p := reflect.ValueOf(&x)
//
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
// 这也是一种获取指针对象所指向的实际值的方法,这里换位.Elem也是可以的。
indirect := reflect.Indirect(p)
if indirect.CanSet() {
indirect.SetFloat(7.1)
}
println(x)
}
需要注意:利用反射修改值的时候需要传递指针
反射可以获取结构体里面的字段,tag,方法等等。下面的例子中,获取字段,获取结构体,调用方法。
type Kinds struct {
Name string
Age int
price float64
}
func (k *Kinds) Names() {
println(k.Name,"llalla")
}
func main() {
kinds := Kinds{
Name: "12",
Age: 1,
Price: 2,
}
// 传递的是指针,先获取指针对象所对应的反射对象,在通过Elem获取真正的对象
of := reflect.ValueOf(&kinds).Elem()
// 或者真正对象的类型
t := of.Type()
// 获取字段的数量
for i := 0; i < t.NumField(); i++ {
// 获取字段,这里是直接在value上获取的
field := of.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
// 字段的名称,注意,这里是在Type对象上获取的,要记住Type保存类型信息,Value爆粗实际的值类型
t.Field(i).Name,
// 在value上获取
field.Type(),
// 这里最重要,只有value对象持有真正的数据
field.Interface())
}
// 获取指针对象所对应的反射对象
valueOf := reflect.ValueOf(&kinds)
t2 := valueOf.Type()
// 这里要注意,方法是在 kinds的指针对象上面的,不是在Kind上,所以,如果通过 reflect.ValueOf(&kinds).Elem().Type().NumMethod() 结果为0
for i := 0; i < t2.NumMethod(); i++ {
method := t2.Method(i)
value := method.Func
// 调用方法,函数和方法是不一样的,第一个参数为作用域,也就是接受对象,如果是函数的话是不需要的。
value.Call([]reflect.Value{valueOf})
}
}
代码上写了注释,要注意以下几点:
方法的接受对象要分清(指针还是非指针)
操作反射对象时要将Type和Value的作用分清,以及自己在操作的时候操作的是什么,比如获取字段的值就不能从Type对象中获取。
在go中可以看到Python的影子,在go中类型是很灵活的。函数,方法也是有value的。通过反射来调用函数,代码如下:
func add(a,b int) int {
return a+b
}
func main() {
// 获取add方法对应的反射对象
valueOf := reflect.ValueOf(add)
// 组装参数调用,要注意,函数和方法调用的区别,方法有接收者的。Call的第一个参数是接收者
values := []reflect.Value{
reflect.ValueOf(10),
reflect.ValueOf(12),
}
res := valueOf.Call(values)
for _, item := range res {
println(item.Int())
}
}
博客参考laws-of-reflection
例子单独写一章
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。