Go系列之 反射

wuqinqiang · · 378 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

图片

反射增强了语言的动态描述能力,你要问我什么是动态,我只能说,所有可能产生意料之外情理之中的变化,都是动态。

概述

反射这个词并不是特定语言持有的,相反很多语言拥有着自己的反射模型。老实说,我并不喜欢去用专业的术语去解释一些概念性的东西,这样往往观看的人也云里雾里,这些概念性的东西,每个人脑海中都有自己的“解释语言“,随他去吧。

我主要想谈谈为什么需要反射,应用场景是什么?其实在我看来,这两个问题严格意义上是等价的,即 为什么=应用场景,应用场景=为什么。

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...


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

本文来自:Segmentfault

感谢作者:wuqinqiang

查看原文:Go系列之 反射

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

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