go反射,看这篇就够了

写在前面

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

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

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

类型和值

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

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

go反射,看这篇就够了_第1张图片

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
}

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

go反射,看这篇就够了_第2张图片

每种类型可以使用的方法都是不一样的,错误的使用会引发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)
}

go反射,看这篇就够了_第3张图片

反射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
  • 可以使用断言代替的话,就不要使用反射
  • 如果有可能应当避免使用反射

参考资料

The Go Blog

反射

你可能感兴趣的:(go,golang)