Go系列之反射

Remember · · 699 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/1M4skfznvt.webp!large) > 反射增强了语言的动态描述能力,你要问我什么是动态,我只能说,所有可能产生意料之外情理之中的变化,都是动态。 **概述** 反射这个词并不是特定语言持有的,相反很多语言拥有着自己的反射模型。老实说,我并不喜欢去用专业的术语去解释一些概念性的东西,这样往往观看的人也云里雾里,这些概念性的东西,每个人脑海中都有自己的“解释语言“,随他去吧。 我主要想谈谈为什么需要反射,应用场景是什么?其实在我看来,这两个问题严格意义上是等价的,即 为什么=应用场景,应用场景=为什么。 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 } ``` 可以看到,这个结构体的三个字段都是私有的,没有对外暴露任何字段,但是它提供了一系列获取数据和写入等操作的方法。 ![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/o3C6PNmirP.webp!large) **反射三大定律** 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) } ``` 程序打印结果: ![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/xhTtL9eF6e.png!large) 我们定义了两个变量,他们的类型分别是 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) } } ``` 运行结果: ![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/ymASwzhvBI.png!large) 上面就不解释了,主要解释循环里面的,我们通过 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)。 ![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/kMlU2VsF7k.png!large) 错误的原因正是值是不可修改的。 反射对象是否可修改取决于其所存储的值,上面 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 。 运行结果值已然被修改。 ![图片](https://cdn.learnku.com/uploads/images/202012/12/26855/rBmHF2PfmK.png!large) **结尾** 关于反射,我还想说说它不好的地方: - 作为静态语言,编码过程中,编译器可以提前发现一些类型错误,但是反射代码是不行的(如果可以请告知)。可能会因为 bug 导致运行恐慌。 - 反射对性能影响比较大,对于一些注重运行效率的关键点,尽量避免使用反射。 还有其他有趣的操作,推荐先多看几遍官方的一篇博客: https://blog.golang.org/laws-of-reflection 参考资料: https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/#%E7%AC%AC%E4%BA%8C%E6%B3%95%E5%88%99 https://www.bookstack.cn/read/GoExpertProgramming/chapter06-6.1-reflect.md

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

699 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传