聊一聊 Go 语言的反射

楔子

本次来聊一聊反射,那什么是反射呢?用大白话解释就是,程序在运行期间可以动态地查看某个变量值的类型,并且还能够动态调用、修改自身的行为。Python 应该是反射机制最为彪悍的语言了,当然查看自身类型更是不在话下,这一点动态语言显然占据绝对的优势。而 Go 虽然作为静态语言,但也是支持反射的,主要通过 reflect 包实现,并且功能还很强大。

那么 Go 的反射是如何定义的呢?Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

为什么要有反射?

有以下两个场景:

  • 一个函数接收不同类型的参数要表现不同的行为;
  • 根据外界返回的值的类型不同, 表现的行为也不同;

举个栗子:

package main

import "fmt"

func foo(a interface{}) {
    // 这里使用断言,先不使用反射
    switch t := a.(type) {
    case int:
        fmt.Printf("t 是整型,值为 %d\n", t)
    case string:
        fmt.Printf("t 是字符串,值为 %s\n", t)
    case float64:
        fmt.Printf("t 是浮点型,值为 %f\n", t)
    default:
        fmt.Println("我也不知道什么类型")
    }
}

func main() {
    var v interface{} = "abc"
    foo(v) // t 是字符串,值为 abc
}

目前我们是通过类型断言来实现的,后面使用反射也是类似的。反射很强大,但是不建议乱用。原因如下:

  • 反射相关的代码比较难以阅读, 代码的可读性也是我们需要考虑的;
  • 反射的过程如果出错了, 那么不好意思, 只会在运行时才会报错, 在编译的时候是检测不出来的。这就可能导致, 你的服务都已经完美运行一个月了, 但是有一天却突然挂了。如果出现这种情况, 那么你要好好想想是不是你的反射相关的代码出问题了;
  • 反射是动态地检测变量的值的类型, 这个是很损失效率的, 如果没必要, 那么尽量不要使用反射;

反射是如何实现的呢?

我们上一次提到了 interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

package main

import "fmt"


func main() {
    var v interface{} = 123
    // 我们给 v 这个接口赋值为 123, 那么存储的实体的类型就已经决定了, 是根据赋的值推断出来的
    // 只不过我们不知道罢了(但站在上帝视角我们知道 123 是一个整型)
    fmt.Println(v.(int))  // 123
    // 所以在断言的时候, 如果指定为字符串类型、浮点型都是不正确的,虽然我们知道 123 完全可以当成浮点
    // 但还是那句话, 在将 123 赋值给接口 v 的时候, 存储的实体的类型就已经决定了
    // 而 123 显然会被推断成整型, 所以断言它是浮点型就是有问题的
}

Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

reflect.Type 和 reflect.Value

Go 语言中描述一个变量有两种方式:

  • reflect.Type:通过 reflect.TypeOf() 函数获取;
  • reflect.Value:通过 reflect.ValueOf() 函数获取;

Type 和 Value 分别是从一个变量的类型和值来阐述其特征。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    var b = "satori"
    var c = 3.14
    var d = [3]int{1, 2, 3}
    var e = []int{1, 2, 3}
    var f = map[int]int{1: 2, 2: 3}
    var g = struct {
        name string
        age  int
    }{name: "mashiro", age: 17}
    var h = new(int)
    var i = make(chan int)
    
    fmt.Println(reflect.TypeOf(a), reflect.ValueOf(a)) // int 123
    fmt.Println(reflect.TypeOf(b), reflect.ValueOf(b)) // string satori
    fmt.Println(reflect.TypeOf(c), reflect.ValueOf(c)) // float64 3.14
    fmt.Println(reflect.TypeOf(d), reflect.ValueOf(d)) // [3]int [1 2 3]
    fmt.Println(reflect.TypeOf(e), reflect.ValueOf(e)) // []int [1 2 3]
    fmt.Println(reflect.TypeOf(f), reflect.ValueOf(f)) // map[int]int map[1:2 2:3]
    // 匿名结构体会直接返回定义的结构体,如果有名字直接返回名字
    fmt.Println(reflect.TypeOf(g), reflect.ValueOf(g)) // struct { name string; age int } {mashiro 17}
    fmt.Println(reflect.TypeOf(h), reflect.ValueOf(h)) // *int 0xc0000aa008
    fmt.Println(reflect.TypeOf(i), reflect.ValueOf(i)) // chan int 0xc00008c060
}

我们看到对于 TypeOf 函数来说,你的变量定义的时候是什么类型,那么返回的就是什么类型。只不过此时返回的是一个 Type,是一个 interface,你不能直接和字符串去比较,我们可以调用 string 方法转换成字符串。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    var b = struct {
        name string
        age int
    }{}
    fmt.Println(reflect.TypeOf(a).String()) // int
    fmt.Println(reflect.TypeOf(b).String()) // struct { name string; age int }
}

而对于 ValueOf 函数来说,返回的就是你的这个变量的值。但是需要注意的是,这个值是一个 Value 类型,也是一个 interface,想使用的话是需要转化的。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    var b = "satori"
    var c = 3.14
    var d = [3]int{1, 2, 3}
    var e = []int{1, 2, 3}
    var f = map[int]int{1: 2, 2: 3}
    var g = struct {
        name string
        age  int
    }{name: "mashiro", age: 17}
    var h = new(int)
    var i = make(chan int)
    
    // 这里直接调用了 Int(),是因为我们知道这里返回的是 int
    // 如果你能确定值的类型,那么可以直接转化
    fmt.Println(reflect.ValueOf(a).Int()) // 123
    // 我们知道 b 就是一个 string,那么可以直接通过调用 String() 方法来转
    fmt.Println(reflect.ValueOf(b).String()) // satori
    
    // 假设变量 c 我们不确定类型,那么可以通过如下方法
    // 先转换成 interface{},然后采用类型断言的方式就可以了
    switch t := reflect.ValueOf(c).Interface().(type) {
    case int:
        fmt.Println("int", t)
    case float64:
        fmt.Println("float", t) // float 3.14
    }
    
    // 而 d 是一个数组,这种类型,我们没办法直接调用函数转化了
    // 这个时候只能通过 interface{} 的方式。同理切片、map 等等也是如此
    fmt.Println(reflect.ValueOf(d).Interface().([3]int))      // [1 2 3]
    fmt.Println(reflect.ValueOf(e).Interface().([]int))       // [1 2 3]
    fmt.Println(reflect.ValueOf(f).Interface().(map[int]int)) // map[1:2 2:3]
    // 这里必须写上字段名,而且不能写错,否则也会断言失败
    // 总而言之,你定义的时候写的什么类型,就是什么类型
    fmt.Println(reflect.ValueOf(g).Interface().(struct {
        name string
        age  int
    }))  //{mashiro 17}
    fmt.Println(reflect.ValueOf(h).Interface().(*int))     //0xc000062080
    fmt.Println(reflect.ValueOf(i).Interface().(chan int)) //0xc00004c060
}

然后我们来看一下,Type 接口在底层的定义。

type Type interface {
    //支持很多方法,但是不是每一个都常用
    
    //此类型的变量对齐后所占用的字节数
    Align() int
    //struct 的字段对齐后占用的字节数
    FieldAlign() int
    //返回类型方法集里的第 `i` (传入的参数)个方法
    Method(int) Method
    // 通过名称获取方法
    MethodByName(string) (Method, bool)
    // 获取类型方法集里导出的方法个数
    NumMethod() int
    // 类型名称
    Name() string
    // 返回类型所在的路径,如:encoding/base64
    PkgPath() string
    // 返回类型的大小,和 unsafe.Sizeof 功能类似
    Size() uintptr
    //返回类型的字符串表示形式
    String() string
    //返回类型的类型值
    Kind() Kind
    // 类型是否实现了接口 u
    Implements(u Type) bool
    // 是否可以赋值给 u
    AssignableTo(u Type) bool
    // 是否可以类型转换成 u
    ConvertibleTo(u Type) bool
    // 类型是否可以比较
    Comparable() bool
    
    // 下面这些函数只有特定类型可以调用
    // 如:Key, Elem 两个方法就只能是 Map 类型才能调用
    
    // 类型所占据的位数
    Bits() int
    // 返回通道的方向,只能是 chan 类型调用
    ChanDir() ChanDir
    // 返回类型是否是可变参数,只能是 func 类型调用
    // 比如 t 是类型 func(x int, y ... float64)
    // 那么 t.IsVariadic() == true
    IsVariadic() bool
    // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用
    Elem() Type
    // 返回结构体类型的第 i 个字段,只能是结构体类型调用
    // 如果 i 超过了总字段数,就会 panic
    Field(i int) StructField
    // 返回嵌套的结构体的字段
    FieldByIndex(index []int) StructField
    // 通过字段名称获取字段
    FieldByName(name string) (StructField, bool)
    // FieldByNameFunc returns the struct field with a name
    // 返回名称符合 func 函数的字段
    FieldByNameFunc(match func (string) bool) (StructField, bool)
    // 获取函数类型的第 i 个参数的类型
    In(i int) Type
    // 返回 map 的 key 类型,只能由类型 map 调用
    Key() Type
    //返回 Array 的长度,只能由类型 Array 调用
    Len() int
    //返回类型字段的数量,只能由类型 Struct 调用
    NumField() int
    //返回函数类型的输入参数个数
    NumIn() int
    //返回函数类型的返回值个数
    NumOut() int
    //返回函数类型的第 i 个值的类型
    Out(i int) Type
    //返回类型结构体的相同部分
    common() *rtype
    //返回类型结构体的不同部分
    uncommon() *uncommonType
}

至于 Value 接口,里面同样定义了很多的方法,比如我们上面说的:Int(),String(),Interface() 等等,也包括 Type 里面的很多方法。

Type 和 Kind

另外各位是不是发现我们定义的是什么类型,Type 在形式上也是什么类型这种形式虽然保持了一致性,但是不是也有点不好呢?比如说,我们想判断一个函数返回的到底是一个数组还是结构体(假设有一个这样的需求),但数组个数不同、结构体字段个数不同、类型不同、对了还有名字不同,都会返回不同的结果,难道这些在断言的时候都要写一遍吗?数组可能有 100 个元素,难道光数组我们就要写 100 个 case 语句?

显然我们不希望这样的结果,我们希望不管数组元素多少个,只要是一个数组,那么返回一个 array 就好了,如果是一个结构体,那么返回 struct 就行。同理指针也是,我们不在乎到底是什么类型的指针,只要是指针,那么就返回一个 pointer,表示是一个指针就行。那么能不能实现呢?显然可以,从变量的类型来描述在 Go 中除了 Type 之外还有 Kind。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    var b = "satori"
    var c = 3.14
    var d = [3]int{1, 2, 3}
    var e = []int{1, 2, 3}
    var f = map[int]int{1: 2, 2: 3}
    var g = struct {
        name string
        age  int
    }{name: "mashiro", age: 17}
    var h = new(int)
    var i = make(chan int)
    
    // Type 是一个接口,返回的结构体实现了它的所有方法
    // 而调用 Kind 方法就能拿到我们想要的结果
    fmt.Println(reflect.TypeOf(a), reflect.TypeOf(a).Kind()) //int int
    fmt.Println(reflect.TypeOf(b), reflect.TypeOf(b).Kind()) //string string
    fmt.Println(reflect.TypeOf(c), reflect.TypeOf(c).Kind()) //float64 float64
    fmt.Println(reflect.TypeOf(d), reflect.TypeOf(d).Kind()) //[3]int array
    fmt.Println(reflect.TypeOf(e), reflect.TypeOf(e).Kind()) //[]int slice
    fmt.Println(reflect.TypeOf(f), reflect.TypeOf(f).Kind()) //map[int]int map
    fmt.Println(reflect.TypeOf(g), reflect.TypeOf(g).Kind()) //struct { name string; age int } struct
    fmt.Println(reflect.TypeOf(h), reflect.TypeOf(h).Kind()) //*int ptr
    fmt.Println(reflect.TypeOf(i), reflect.TypeOf(i).Kind()) //chan int chan
}

我们看到,除了 int、string、float 之外,其它的都变了。

  • 数组一律返回 array;
  • 切片一律返回 slice;
  • map 一律返回 map;
  • 结构体一律返回 struct;
  • 指针一律返回 ptr;
  • channel 一律返回 chan;

但是注意:这些返回的是一个 Kind 类型,我们还是可以调用 String 方法转成字符串进行比较,当然也可以直接比较。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    var b = "satori"
    var c = 3.14
    var d = [3]int{1, 2, 3}
    var e = []int{1, 2, 3}
    var f = map[int]int{1: 2, 2: 3}
    var g = struct {
        name string
        age  int
    }{name: "mashiro", age: 17}
    var h = new(int)
    var i = make(chan int)
    
    // 调用 String() 得到字符串
    fmt.Println(reflect.TypeOf(a).Kind().String() == "int") //true
    // 也可以直接比较,使用 reflect,里面有很多类型
    fmt.Println(reflect.TypeOf(b).Kind() == reflect.String)  //true
    fmt.Println(reflect.TypeOf(c).Kind() == reflect.Float64) //true
    fmt.Println(reflect.TypeOf(d).Kind() == reflect.Array)   //true
    fmt.Println(reflect.TypeOf(e).Kind() == reflect.Slice)   //true
    fmt.Println(reflect.TypeOf(f).Kind() == reflect.Map)     //true
    fmt.Println(reflect.TypeOf(g).Kind() == reflect.Struct)  //true
    fmt.Println(reflect.TypeOf(h).Kind() == reflect.Ptr)     //true
    fmt.Println(reflect.TypeOf(i).Kind() == reflect.Chan)    //true
}

而且这个 Kind() 方法除了 reflect.Type 可以调用之外,reflect.Value 也是可以调用的。并且 reflect.Value 调用 Type 方法,还能够得到 reflect.Type。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    // reflect.TypeOf(a) 返回一个 Type,通过 reflect.ValueOf(a) 得到一个 Value
    // 然后再手动调用 Type() 方法得到 Type 也是可以的
    fmt.Println(reflect.TypeOf(a) == reflect.ValueOf(a).Type()) //true
    // 得到 Kind,除了对 Type 调用 Kind() 方法之外,还可以通过对 Value 调用 Kind() 方法获取
    fmt.Println(reflect.TypeOf(a).Kind() == reflect.ValueOf(a).Kind()) //true
    //同理,故意兜个圈子也行
    fmt.Println(reflect.TypeOf(a).Kind() == reflect.ValueOf(a).Type().Kind()) //true
} 

一张图来表示 Type、Value、Kind 三者的关系。

通过反射修改原对象

通过反射还可以实现对象的修改,但要求反射变量必须是可设置的。怎么理解呢?我们通过修改反射变量,会影响原变量,就称之为反射变量是可设置的。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a = 123
    // 此时的 v 就是反射变量,如果这里我们传递了 a,而不是 a 的指针,那么调用 Elem() 方法是会报错的
    // 因为 Go 是值传递,所以获取了反射变量也不会影响原来的变量,因此这里必须要传递指针
    // 传递了还不够,还必须要调用 Elem 方法,此时的 v 才能真正的代表 a
    v := reflect.ValueOf(&a).Elem()
    // 调用 CanSet,表示该反射变量是否是可设置的
    fmt.Println(v.CanSet()) //true
    
    // 设置值
    v.SetInt(456)
    // 此时的 a 已经变了
    // 总之就如同函数传参一样,如果想要修改原变量,那么反射变量必须获得原变量的地址才行
    fmt.Println(a) //456
}

当然我们还可以对结构体进行操作:

package main

import (
    "fmt"
    "reflect"
)

type girl struct {
    name string
    age  int `json:"年龄"`
}

func main() {
    var g = girl{"mashiro", 17}
    // 如果是为了拿到值的话,那么使用 reflect.ValueOf(&g).Elem()
    // 这里查看字段属性使用了 reflect.TypeOf(&g).Elem()
    // 另外 reflect.TypeOf(&g).Elem() 等价于 reflect.ValueOf(&g).Type().Elem()
    reflect_g := reflect.TypeOf(&g).Elem()
    // 通过字段名查找相应字段
    s, _ := reflect_g.FieldByName("age")
    fmt.Println(s.Tag.Get("json")) //年龄
}

反射获取结构体的属性

再来看看结构体:

package main

import (
    "fmt"
    "reflect"
)

type girl struct {
    name   string
    age    int
    gender string
}

func (self girl) Foo1()  { fmt.Println("Foo1") }
func (self girl) Foo2()  { fmt.Println("Foo2") }
func (self *girl) Foo3() { fmt.Println("Foo3") }
func (self girl) Foo4()  { fmt.Println("Foo4") }

func main() {
    g := girl{"mashiro", 17, "f"}
    //查看有多少个字段,以下几种方式是一样的
    t1, t2 := reflect.TypeOf(g), reflect.TypeOf(&g).Elem()
    v1, v2 := reflect.ValueOf(g), reflect.ValueOf(&g).Elem()
    fmt.Println(t1.NumField(), t2.NumField()) //3 3
    fmt.Println(v1.NumField(), v2.NumField()) //3 3
    
    //查看这个结构体绑定了多少个方法,我们看到指针接收者是没有计算在内的
    fmt.Println(t1.NumMethod(), t2.NumMethod()) //3 3
    fmt.Println(v1.NumMethod(), v2.NumMethod()) //3 3
}

可能有人搞不清,针对于结构体到底什么时候用 reflect.TypeOf,什么时候用 reflect.ValueOf。我想说的是,对于 Type 和 Value 来说,它们有很多方法都是相同的,比如这里的查看字段个数、方法个数。当然还有很多名字相同,但是作用不同的方法,比如 FieldByName、FieldByIndex 等等,以及调用 Field(i) 获取第几个字段。

虽然方法名一样,但是 Type 调用返回的是一个 StructField,用于描述结构体对应字段的信息的,比如字段名等等。而 Value 调用返回的是一个 Value 类型,是用来获取结构体对应字段的值的。

我们举个例子:

package main

import (
    "fmt"
    "reflect"
)

type girl struct {
    Name   string
    Age    int
    Gender string
}

func (self girl) Foo1()  { fmt.Println("Foo1") }
func (self girl) Foo2()  { fmt.Println("Foo2") }
func (self *girl) Foo3() { fmt.Println("Foo3") }
func (self girl) Foo4()  { fmt.Println("Foo4") }

func main() {
    g := girl{"mashiro", 17, "f"}
    ref := reflect.TypeOf(g)
    for i := 0; i < ref.NumField(); i++ {
        //Field 表示获取指定字段,i 表示索引
        //这里获取的 t 是一个 StructField,包含了字段的信息
        t := ref.Field(i)
        fmt.Println(t.Name)
        /*
        Name
        Age
        Gender
        */
    }

    val := reflect.ValueOf(g)
    for i := 0; i < val.NumField(); i++ {
        //Field 作用同上,但这个是 Value 调用的 Field
        //返回的不再是 StructField,而是一个 Value,我们可以直接拿到值
        t := val.Field(i)
        fmt.Println(t.Interface())
        /*
        mashiro
        17
        f
        */
    }

}

还可以作用于方法:

package main

import (
    "fmt"
    "reflect"
)

type girl struct {
    Name   string
    Age    int
    Gender string
}

func (self girl) Foo1()  { fmt.Println("Foo1") }
func (self girl) Foo2()  { fmt.Println("Foo2") }
func (self *girl) Foo3() { fmt.Println("Foo3") }
func (self girl) Foo4()  { fmt.Println("Foo4") }

func main() {
    g := girl{"mashiro", 17, "f"}
    ref := reflect.TypeOf(g)
    //当然也可以针对于方法
    for i := 0; i < ref.NumMethod(); i++ {
        t := ref.Method(i)
        fmt.Println(t.Name)
        /*
           Foo1
           Foo2
           Foo4
        */
    }
    
    val := reflect.ValueOf(g)
    for i := 0; i < val.NumMethod(); i++ {
        t := val.Method(i)
        //t.Call调用函数,里面接收一个[]Value,返回一个[]Value
        t.Call([]reflect.Value{})
        /*
           Foo1
           Foo2
           Foo4
        */
    }
    
}

反射修改结构体的字段的值

当我们想修改结构体指定字段的值,用反射该如何实现呢?

package main

import (
    "fmt"
    "reflect"
)

type girl struct {
    Name   string
    Age    int
    Gender string
}

func main() {
    g := girl{"mashiro", 17, "f"}
    //修改值的话,肯定要用 ValueOf。因为 TypeOf 是用来获取字段本身属性的
    v := reflect.ValueOf(&g).Elem()
    // 是可以设置的
    fmt.Println(v.FieldByName("Age").CanSet()) //true
    //设置值
    v.FieldByName("Name").SetString("satori")
    //打印发现已经被修改了
    fmt.Println(g)  // {satori 17 f}
}

但需要注意的是,利用反射机制,对于结构体中未导出成员,可以读取,但不能修改其值。也就是说,通过反射,结构体中可以被修改的成员只有是导出成员,也就是字段名的首字母是大写的。

小结

反射的话,难度稍微有点大。与其说是难度大,倒不如说是比较杂,因为反射可以作用的类型太多了,所以可以自己多试试感受一下。

另外,虽然反射比较高端,但是不要乱用哦。

posted @ 2019-09-27 16:16  古明地盆  阅读(3369)  评论(1编辑  收藏  举报