6

Go系列之 反射

 3 years ago
source link: https://studygolang.com/articles/32120
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

J3q6Nzn.png!mobile

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

概述

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

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

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
}

可以看到,这个结构体的三个字段都是私有的,没有对外暴露任何字段,但是它提供了一系列获取数据和写入等操作的方法。

ju6Njuj.png!mobile

反射三大定律

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)
}

程序打印结果:

fU363qr.png!mobile

我们定义了两个变量,他们的类型分别是 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)
  }
}

运行结果:

ryQ7rqy.png!mobile

上面就不解释了,主要解释循环里面的,我们通过 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)。

u6JV3yr.png!mobile

错误的原因正是值是不可修改的。

反射对象是否可修改取决于其所存储的值,上面 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 。

运行结果值已然被修改。

2IFFNfq.png!mobile

结尾

关于反射,我还想说说它不好的地方:

  • 作为静态语言,编码过程中,编译器可以提前发现一些类型错误,但是反射代码是不行的(如果可以请告知)。可能会因为 bug 导致运行恐慌。
  • 反射对性能影响比较大,对于一些注重运行效率的关键点,尽量避免使用反射。

还有其他有趣的操作,推荐先多看几遍官方的一篇博客:

https://blog.golang.org/laws-...

参考资料:

https://draveness.me/golang/d...

https://www.bookstack.cn/read...

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

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK