《快学 Go 语言》第 14 课 —— 反射

反射是 Go 语言学习的一个难点,但也是非常重要的一个知识点。反射是洞悉 Go 语言类型系统设计的法宝,Go 语言的 ORM 库离不开它,Go 语言的 json 序列化库离不开它,Go 语言的运行时更是离不开它。笔者在学习反射功能的时候也是费了好大一番功夫才敢说自己确实搞懂了。下面请读者跟着我的步伐来一步一步深入理解反射功能。

反射的目标

反射的目标之一是获取变量的类型信息,例如这个类型的名称、占用字节数、所有的方法列表、所有的内部字段结构、它的底层存储类型等等。

反射的目标之二是动态的修改变量内部字段值。比如 json 的反序列化,你有的是对象内部字段的名称和相应的值,你需要把这些字段的值循环填充到对象相应的字段里。

reflect.Kind

reflect 包定义了十几种内置的「元类型」,每一种元类型都有一个整数编号,这个编号使用 reflect.Kind 类型表示。不同的结构体是不同的类型,但是它们都是同一个元类型 Struct。包含不同子元素的切片也是不同的类型,但是它们都会同一个元类型 Slice。

type Kind uint

const (
    Invalid Kind = iota // 不存在的无效类型
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr // 指针的整数类型,对指针进行整数运算时使用
    Float32
    Float64
    Complex64
    Complex128
    Array // 数组类型
    Chan // 通道类型
    Func  // 函数类型
    Interface  // 接口类型
    Map // 字典类型
    Ptr // 指针类型
    Slice // 切片类型
    String // 字符串类型
    Struct // 结构体类型
    UnsafePointer // unsafe.Pointer 类型
)

反射的基础代码

reflect 包提供了两个基础反射方法,分别是 TypeOf() 和 ValueOf() 方法,分别用于获取变量的类型和值,定义如下

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value

package main

import "fmt"
import "reflect"

func main() {
    var s int = 42
    fmt.Println(reflect.TypeOf(s))
    fmt.Println(reflect.ValueOf(s))
}

--------
int
42

640?wx_fmt=png
图片

TypeOf() 方法返回变量的类型信息得到的是一个类型为 reflect.Type 的变量,ValueOf() 方法返回变量的值信息得到的是一个类型为 reflect.Value 的变量。

reflect.Type

它是一个接口类型,里面定义了非常多的方法用于获取和这个类型相关的一切信息。这个接口的结构体实现隐藏在 reflect 包里,每一种类型都有一个相关的类型结构体来表达它的结构信息。

type Type interface {
  ...
  Method(i int) Method  // 获取挂在类型上的第 i'th 个方法
  ...
  NumMethod() int  // 该类型上总共挂了几个方法
  Name() string // 类型的名称
  PkgPath() string // 所在包的名称
  Size() uintptr // 占用字节数
  String() string // 该类型的字符串形式
  Kind() Kind // 元类型
  ...
  Bits() // 占用多少位
  ChanDir() // 通道的方向
  ...
  Elem() Type // 数组,切片,通道,指针,字典(key)的内部子元素类型
  Field(i int) StructField // 获取结构体的第 i'th 个字段
  ...
  In(i int) Type  // 获取函数第 i'th 个参数类型
  Key() Type // 字典的 key 类型
  Len() int // 数组的长度
  NumIn() int // 函数的参数个数
  NumOut() int // 函数的返回值个数
  Out(i int) Type // 获取函数 第 i'th 个返回值类型
  common() *rtype // 获取类型结构体的共同部分
  uncommon() *uncommonType // 获取类型结构体的不同部分
}

// 基础类型 rtype 实现了 Type 接口
type rtype struct {
  size uintptr // 占用字节数
  ptrdata uintptr
  hash uint32 // 类型的hash值
  ...
  kind uint8 // 元类型
  ...
}

// 切片类型
type sliceType struct {
  rtype
  elem *rtype // 元素类型
}

// 结构体类型
type structType struct {
  rtype
  pkgPath name  // 所在包名
  fields []structField  // 字段列表
}

...

reflect.Value

不同于 reflect.Type 接口,reflect.Value 是结构体类型,一个非常简单的结构体。

type Value struct {
  typ *rtype  // 变量的类型结构体
  ptr unsafe.Pointer // 数据指针
  flag uintptr // 标志位
}

package main

import "reflect"
import "fmt"

func main() {
    type SomeInt int
    var s SomeInt = 42
    var t = reflect.TypeOf(s)
    var v = reflect.ValueOf(s)
 // reflect.ValueOf(s).Type() 等价于 reflect.TypeOf(s)
 fmt.Println(t == v.Type())
    fmt.Println(v.Kind() == reflect.Int) // 元类型
 // 将 Value 还原成原来的变量
 var is = v.Interface()
 fmt.Println(is.(SomeInt))
}

----------
true
true
42

将上面的各种关系整理一下,可以得到下面这张图

640?wx_fmt=png
图片

Value 这个结构体虽然很简单,但是附着在 Value 上的方法非常之多,主要是用来方便用户读写 ptr 字段指向的数据内存。虽然我们也可以通过 unsafe 包来精细操控内存,但是使用过于繁琐,使用 Value 结构体提供的方法会更加简单直接。

 func (v Value) SetLen(n int)  // 修改切片的 len 属性
 func (v Value) SetCap(n int) // 修改切片的 cap 属性
 func (v Value) SetMapIndex(key, val Value) // 修改字典 kv
 func (v Value) Send(x Value) // 向通道发送一个值
 func (v Value) Recv() (x Value, ok bool) // 从通道接受一个值
 // Send 和 Recv 的非阻塞版本
 func (v Value) TryRecv() (x Value, ok bool)
 func (v Value) TrySend(x Value) bool

 // 获取切片、字符串、数组的具体位置的值进行读写
 func (v Value) Index(i int) Value
 // 根据名称获取结构体的内部字段值进行读写
 func (v Value) FieldByName(name string) Value
 // 将接口变量装成数组,一个是类型指针,一个是数据指针
 func (v Value) InterfaceData() [2]uintptr
 // 根据名称获取结构体的方法进行调用
 // Value 结构体的数据指针 ptr 可以指向方法体
 func (v Value) MethodByName(name string) Value
 ...

理解 Go 语言官方的反射三大定律

官方对 Go 语言的反射功能做了一个抽象的描述,总结出了三大定律,分别是

  1. Reflection goes from interface value to reflection object.

  2. Reflection goes from reflection object to interface value.

  3. To modify a reflection object, the value must be settable.

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value

func (v Value) Interface() interface{}

值类型的变量是不可以通过反射来修改,因为在反射之前,传参的时候需要将值变量转换成接口变量,值内容会被浅拷贝,反射对象 Value 指向的数据内存地址不是原变量的内存地址,而是拷贝后的内存地址。这意味着如果值类型变量可以通过反射功能来修改,那么修改操作根本不会影响到原变量的值,那就白白修改了。所以 reflect 包就直接禁止了通过反射来修改值类型的变量。我们看个例子

package main

import "reflect"

func main() {
    var s int = 42
    var v = reflect.ValueOf(s)
    v.SetInt(43)
}

---------
panic: reflect: reflect.Value.SetInt using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignable(0x82)
    /usr/local/go/src/reflect/value.go:234 +0x157
reflect.Value.SetInt(0x107a1a0, 0xc000016098, 0x82, 0x2b)
    /usr/local/go/src/reflect/value.go:1472 +0x2f
main.main()
    /Users/qianwp/go/src/github.com/pyloque/practice/main.go:8 +0xc0
exit status 2

package main

import "fmt"
import "reflect"

func main() {
    var s int = 42
    // 反射指针类型
 var v = reflect.ValueOf(&s)
    // 要拿出指针指向的元素进行修改
 v.Elem().SetInt(43)
    fmt.Println(s)
}

-------
43

结构体也是值类型,也必须通过指针类型来修改。下面我们尝试使用反射来动态修改结构体内部字段的值。

package main

import "fmt"
import "reflect"

type Rect struct {
    Width int
    Height int
}

func SetRectAttr(r *Rect, name string, value int) {
    var v = reflect.ValueOf(r)
    var field = v.Elem().FieldByName(name)
    field.SetInt(int64(value))
}

func main() {
    var r = Rect{50, 100}
    SetRectAttr(&r, "Width", 100)
    SetRectAttr(&r, "Height", 200)
    fmt.Println(r)
}

-----
{100 200}

640?wx_fmt=png

扫一扫二维码,订阅《快学 Go 语言》更多内容

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值