深度剖析Reflect + 实战案例

关键词:go、reflect、反射、实战

写在前面

Go的反射机制带来很多动态特性,一定程度上弥补了Go缺少自定义范型而导致的不便利。

Go反射机制设计的目标之一是任何操作(非反射)都可以通过反射机制来完成

类型和值

变量是由两部分组成:变量的类型和变量的值。

reflect.Typereflect.Value是反射的两大基本要素,他们的关系如下:

  • 任意类型都可以转换成TypeValue
  • Value可以转换成Type
  • Value可以转换成Interface

image-20210307122816992

Type

类型系统

Type描述的是变量的类型,关于类型请参考下面这个文章:Go类型系统概述

Go语言的类型系统非常重要,如果不熟知这些概念,则很难精通Go编程。

Type是什么?

reflect.Type实际上是一个接口,它提供很多api(方法)让你获取变量的各种信息。比如对于数组提供了LenElem两个方法分别获取数组的长度和元素。

type Type interface {
    // Elem returns a type's element type.
    // It panics if the type's Kind is not Array, Chan, Map, Ptr, or Slice.
    Elem() Type

    // Len returns an array type's length.
    // It panics if the type's Kind is not Array.
    Len() int
}

不同类型可以使用的方法如下:

image-20210307122909586

每种类型可以使用的方法都是不一样的,错误的使用会引发panic

思考:为什么array支持Len方法,而slice不支持?

Type有哪些实现?

使用reflect.TypeOf可以获取变量的Type

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i)) // 强制转换成*emptyInterface类型
    return toType(eface.typ)
}

我需要知道TypeOf反射的是变量的类型,而不是变量的值(这点非常的重要)。

  • unsafe.Pointer(&i),先将i的地址转换成Pointer类型
  • (*emptyInterface)(unsafe.Pointer(&i)),强制转换成*emptyInterface类型
  • *(*emptyInterface)(unsafe.Pointer(&i)),解引用,所以eface就是emptyInterface

通过unsafe的骚操作,我们可以将任意类型转换成emptyInterface类型。因为emptyInterface是不可导出的,所以使用toType方法将*rtype包装成可导出的reflect.Type

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

所以,rtype就是reflect.Type的一种实现。

rtype结构解析

下面重点看下rtype结构体:

type rtype struct {
   size       uintptr // 类型占用空间大小
   ptrdata    uintptr // size of memory prefix holding all pointers
   hash       uint32 // 唯一hash,表示唯一的类型
   tflag      tflag // 标志位
   align      uint8 // 内存对其
   fieldAlign uint8
   kind       uint8 // 
   /**
        func (t *rtype) Comparable() bool {
            return t.equal != nil
        }
        */
   equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较函数,是否可以比较
   // gcdata stores the GC type data for the garbage collector.
   // If the KindGCProg bit is set in kind, gcdata is a GC program.
   // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
   gcdata    *byte
   str       nameOff // 字段名称
   ptrToThis typeOff
}

rtype里面的信息包括了:

  • size:类型占用空间的大小(大小特指类型的直接部分,什么是直接部分请参考值部
  • tflag:标志位
    • tflagUncommon: 是否包含一个指针,比如slice会引用一个array
    • tflagNamed:是否是命名变量,如var a = []string[]string就匿名的,a是命名变量
  • hash:类型的hash值,每一种类型在runtime里面都是唯一的
  • kind:底层类型,一定是官方库定义的26个基本内置类型其中之一
  • equal:确定类型是否可以比较

看到这里发现rtype类型描述的信息是有限的,比如一个arraylen是多长,数组元素的类型,都无法体现。你知道这些问题的答案么?

看下Elem方法的实现——根据Kind的不同,可以再次强制转换类型。

func (t *rtype) Elem() Type {
    switch t.Kind() {
    case Array:
        tt := (*arrayType)(unsafe.Pointer(t))
        return toType(tt.elem)
    case Chan:
        tt := (*chanType)(unsafe.Pointer(t))
        return toType(tt.elem)
    ...
}

观察下arrayTypechanType的定义,第一位都是一个rtype。我们可以简单理解,就是一块内存空间,最开头就是rtype,后面根据类型不同跟着的结构也是不同的。(*rtype)(unsafe.Pointer(t))只读取开头的rtype(*arrayType)(unsafe.Pointer(t))强制转换之后,不仅读出了rtype还读出了数组特有的elemslicelen的值。

// arrayType represents a fixed array type.
type arrayType struct {
    rtype
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}

// chanType represents a channel type.
type chanType struct {
    rtype
    elem *rtype  // channel element type
    dir  uintptr // channel direction (ChanDir)
}

image-20210307122933501

反射struct的方法

对于方法有个比较特殊的地方——方法的第一个参数是自己,这点和C相似。

type f struct {
}

func (p f) Run(a string) {

}

func main() {
    p := f{}
    t := reflect.TypeOf(p)
    fmt.Printf("f有%d个方法\n", t.NumMethod())

    m := t.Method(0)
    mt := m.Type
    fmt.Printf("%s方法有%d个参数\n", m.Name, mt.NumIn())
    for i := 0; i < mt.NumIn(); i++ {
        fmt.Printf("\t第%d个参数是%#v\n", i, mt.In(i).String())
    }
}

输出结果为:

f有1个方法
Run方法有2个参数
        第0个参数是"main.f"1个参数是"string"

思考:如果我们将Run方法定义为func (p *f) Run(a string) {},结果会是什么样呢?

Value

明白了Type之后,Value就非常好理解了。直接看下reflect.ValueOf的代码:

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

ValueOf函数很简单,先将i主动逃逸到堆上,然后将i通过unpackEface函数转换成Value

unpackEface函数,(*emptyInterface)(unsafe.Pointer(&i))i强制转换成eface,然后变为Value返回。

Value是什么

value是一个超级简单的结构体,简单到只有3个field

type Value struct {
    // 类型元数据
    typ *rtype

    // 值的地址
    ptr unsafe.Pointer

    // 标识位
    flag
}

看到Value中也包含了*rtype,这就解释了为什么reflect.Value可以直接转换成reflect.Type

堆逃逸

逃逸到堆意味着将值拷贝一份到堆上,这也是反射的主要原因。

func main() {
    var a = "xxx"
    _ = reflect.ValueOf(&a)

    var b = "xxx2"
    _ = reflect.TypeOf(&b)
}

然后想要看到是否真的逃逸,可以使用go build -gcflags -m编译,输出如下:

./main.go:9:21: inlining call to reflect.ValueOf
./main.go:9:21: inlining call to reflect.escapes
./main.go:9:21: inlining call to reflect.unpackEface
./main.go:9:21: inlining call to reflect.(*rtype).Kind
./main.go:9:21: inlining call to reflect.ifaceIndir
./main.go:12:20: inlining call to reflect.TypeOf
./main.go:12:20: inlining call to reflect.toType
./main.go:8:6: moved to heap: a

moved to heap: a这行表明,编译器将a分配在堆上了。

Value settable的问题

先看个例子🌰:

func main() {
    a := "aaa"
    v := reflect.ValueOf(a)
    v.SetString("bbb")
    println(v.String())
}

// panic: reflect: reflect.Value.SetString using unaddressable value

上面的代码会发生panic,原因是a的值不是一个可以settable的值。

v := reflect.ValueOf(a)a传递给了ValueOf函数,在go语言中都是值传递,意味着需要将变量a对应的值复制一份当成函数入参数。此时反射的value已经不是曾今的a了,那我通过反射修改值是不会影响到a。当然这种修改是令人困惑的、毫无意义的,所以go语言选择了报错提醒。

通过反射修改值

既然不能直接传递值,那么就传递变量地址吧!

func main() {
      a := "aaa"
      v := reflect.ValueOf(&a)
    v = v.Elem()
      v.SetString("bbb")
      println(v.String())
}

// bbb
  • v := reflect.ValueOf(&a),将a的地址传递给了ValueOf,值传递复制的就是a的地址。
  • v = v.Elem(),这部分很关键,因为传递的是a的地址,那么对应ValueOf函数的入参的值就是一个地址,地址是禁止修改的。v.Elem()就是解引用,返回的v就是变量a真正的reflection Value

实战

场景:大批量操作的时候,出于性能考虑我们经常需要先进行分片,然后分批写入数据库。那么有没有一个函数可以对任意类型(T)进行分片呢?(类似php里面的array_chunk函数)

代码如下:

// SliceChunk 任意类型分片
// list: []T
// ret: [][]T
func SliceChunk(list interface{}, chunkSize int) (ret interface{}) {
    v := reflect.ValueOf(list)
    ty := v.Type() // []T

    // 先判断输入的是否是一个slice
    if ty.Kind() != reflect.Slice {
        fmt.Println("the parameter list must be an array or slice")
        return nil
    }

    // 获取输入slice的长度
    l := v.Len()

    // 计算分块之后的大小
    chunkCap := l/chunkSize + 1

    // 通过反射创建一个类型为[][]T的slice
    chunkSlice := reflect.MakeSlice(reflect.SliceOf(ty), 0, chunkCap)
    if l == 0 {
        return chunkSlice.Interface()
    }

    var start, end int
    for i := 0; i < chunkCap; i++ {
        end = chunkSize * (i + 1)
        if i+1 == chunkCap {
            end = l
        }
        // 将切片的append到chunk中
        chunkSlice = reflect.Append(chunkSlice, v.Slice(start, end))
        start = end
    }
    return chunkSlice.Interface()
}

因为返回值是一个interface,需要使用断言来转换成目标类型。

var phones  = []string{"a","b","c"}
chunks := SliceChunk(phones, 500).([][]string)

总结

虽然反射很灵活(几乎可以干任何事情),下面有三点建议:

  • 可以只使用reflect.TypeOf的话,就不要使用reflect.ValueOf
  • 可以使用断言代替的话,就不要使用反射
  • 如果有可能应当避免使用反射

参考资料

[1] The Go Blog
[2] 反射

本作品采用《CC 协议》,转载必须注明作者和本文链接
您的点赞、评论和关注,是我创作的不懈动力。 学无止境,让我们一起加油,在技术的胡同里越走越深!
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!