反射增强了语言的动态描述能力,你要问我什么是动态,我只能说,所有可能产生意料之外情理之中的变化,都是动态。
概述
反射这个词并不是特定语言持有的,相反很多语言拥有着自己的反射模型。老实说,我并不喜欢去用专业的术语去解释一些概念性的东西,这样往往观看的人也云里雾里,这些概念性的东西,每个人脑海中都有自己的“解释语言“,随他去吧。
我主要想谈谈为什么需要反射,应用场景是什么?其实在我看来,这两个问题严格意义上是等价的,即 为什么=应用场景,应用场景=为什么。
go 作为静态类型语言,如果没有反射,很多能够作用于 "任意类型" 的函数,实现起来会很麻烦。我举一个最简单的场景:
package main
// 会员
type member struct {
Name string
Level int
}
// 游客
type visitor struct {
NickName string
}
func main() {
m := member{
Name: "wqq",
Level: 520,
}
v := visitor{
NickName: "curry",
}
checkSomeTing(m)
checkSomeTing(v)
}
func checkSomeTing(v interface{}) {
if "如果是会员的话" {
// ...
} else {
// ...
}
}
上面 member 和 visitor 两种结构体表示两种身份,他们都需要经过公共的 checkSomeTing 操作,我们希望在这个函数中能根据不同的结构体,操作不同的逻辑。如果没有反射,如何知道传进来具体是什么类型的值。(我只是单纯的指出没有反射情况下的问题,而不是吐槽上面这个设计)。
因此,我们需要能有一种方法,它可以在程序运行时获取传入参数真正的类型,如果是 struct 那么这个 struct 有哪些属性,哪些方法......。
Interface
说起 go 反射,就必然绕不开 interface 类型。在 go 中 interface 是一种特殊的类型,可以存放任何实现了其方法的值。如果是一个空 interface ,意味着可以传递任意类型值。interface 类型有(value,type) 对,而反射就是检查 interface 的 (value,type) 对,所有的反射包中的方法,都是围绕 reflect.Type 和 reflect.Value 进行的。reflect.Type 是一个接口类型,提供了一系列获取 interface 信息的接口。源码位置在 src/reflect/type.go。
type Type interface {
// Methods applicable to all types.
Align() int
// FieldAlign returns the alignment in bytes of a value of
// this type when used as a field in a struct.
FieldAlign() int
Method(int) Method
String() string
........
}
而 reflect.Value 的类型被声明成结构体。源码位置 src/reflect/value.go
type Value struct {
// typ holds the type of the value represented by a Value.
typ *rtype
// Pointer-valued data or, if flagIndir is set, pointer to data.
// Valid when either flagIndir is set or typ.pointers() is true.
ptr unsafe.Pointer
flag
}
可以看到,这个结构体的三个字段都是私有的,没有对外暴露任何字段,但是它提供了一系列获取数据和写入等操作的方法。
反射三大定律
go 官方提供了三大定律来说明反射,我们也从这三个定律中学习如何 使用反射。
- 定律一:反射可以将 interface 变量转换为反射对象。
package main
import (
"fmt"
"reflect"
)
func main() {
var a float64 = 32
var b int64 = 32
doSomeTing(a)
doSomeTing(b)
}
func doSomeTing(res interface{}) {
t := reflect.TypeOf(res)
v := reflect.ValueOf(res)
fmt.Println("类型是:", t)
fmt.Println("值是:", v)
}
程序打印结果:
我们定义了两个变量,他们的类型分别是 float64 和 int64 ,传入 doSomeTing 函数,此函数参数类型为空的 interface ,因此可以接收任意类型参数,最终我们通过 reflect.TypeOf 获取了变量的真实类型,通过 reflect.ValueOf 获取变量真实的值。
我们可以再试试通过结构体使用其他操作方法。
package main
import (
"fmt"
"reflect"
)
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
filed := t.Field(i)
value := v.Field(i).Interface()
fmt.Printf("field:%v type:%v value:%v\n", filed.Name, filed.Type, value)
}
}
运行结果:
上面就不解释了,主要解释循环里面的,我们通过 reflect.type 的 NumField 获取结构体中个数,通过 reflect.type 的 Field 方法下标获取属性名,通过 interface() 获取对应属性值。
- 定律二:反射可以将反射对象还原成 interface 对象。
package main
import (
"fmt"
"reflect"
)
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
var user2 user = v.Interface().(user)
fmt.Printf("用户:%+v\n",user2)
}
u 变量转换成反射对象 v,v 又通过 interface() 接口转换成 interface 对象,再通过显性类型转换成 user 结构体对象,赋值给类型为 user 的变量 user2 。
- 定律三:反射对象是否可修改,取决于 value 值是否可设置
我们在通过反射将任意类型的变量(不管什么类型最终传递到 reflect.TypeOf 或者 reflect.ValueOf 都会隐式转换成 interface)转换成反射对象,那么理论上我们就可以基于反射对象设置其所持有的值。
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
v.FieldByName("Name").SetString("curry")
fmt.Printf("v的值:%+v",v)
}
上面代码我们想的是通过反射对象修改结构体属性 Name 值为 curry 。当运行这段代码时,会报运行恐慌(panic)。
错误的原因正是值是不可修改的。
反射对象是否可修改取决于其所存储的值,上面 reflect.ValueOf 传入的其实是 u 的值,而非它的本身。(想想函数传值的时候是传值还是传址),既然是传值,那么通过修改 v 的值当然不可能修改到 u 的值,我们要设置的应该是指针所指向的内容,即 *u 。
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(&u)
v.Elem().FieldByName("Name").SetString("curry")
fmt.Printf("v的值:%+v",v)
}
首先我们通过 &u 传入 u 变量实际存储的地址。然后通过反射对象中的 Elem() 获得指针所指向的 value 。
运行结果值已然被修改。
结尾
关于反射,我还想说说它不好的地方:
- 作为静态语言,编码过程中,编译器可以提前发现一些类型错误,但是反射代码是不行的(如果可以请告知)。可能会因为 bug 导致运行恐慌。
- 反射对性能影响比较大,对于一些注重运行效率的关键点,尽量避免使用反射。
还有其他有趣的操作,推荐先多看几遍官方的一篇博客:
https://blog.golang.org/laws-...
参考资料:
https://draveness.me/golang/d...
https://www.bookstack.cn/read...
有疑问加站长微信联系(非本文作者)