Go的反射机制带来很多动态特性,一定程度上弥补了Go缺少自定义范型而导致的不便利。
Go反射机制设计的目标之一是任何操作(非反射)都可以通过反射机制来完成。
变量是由两部分组成:变量的类型和变量的值。
reflect.Type
和reflect.Value
是反射的两大基本要素,他们的关系如下:
Type
和Value
Value
可以转换成Type
Value
可以转换成Interface
Type
描述的是变量的类型,关于类型请参考下面这个文章:
Go类型系统概述
Go语言的类型系统非常重要,如果不熟知这些概念,则很难精通Go编程。
reflect.Type
实际上是一个接口,它提供很多api
(方法)让你获取变量的各种信息。比如对于数组提供了Len
和Elem
两个方法分别获取数组的长度和元素。
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
}
不同类型可以使用的方法如下:
每种类型可以使用的方法都是不一样的,错误的使用会引发panic
。
思考:为什么
array
支持Len
方法,而slice
不支持?
使用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
结构体:
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
里面的信息包括了:
slice
会引用一个array
var a = []string
,[]string
就匿名的,a是命名变量看到这里发现rtype
类型描述的信息是有限的,比如一个array
的len
是多长,数组元素的类型,都无法体现。你知道这些问题的答案么?
看下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)
...
}
观察下arrayType
和chanType
的定义,第一位都是一个rtype
。我们可以简单理解,就是一块内存空间,最开头就是rtype
,后面根据类型不同跟着的结构也是不同的。(*rtype)(unsafe.Pointer(t))
只读取开头的rtype
,(*arrayType)(unsafe.Pointer(t))
强制转换之后,不仅读出了rtype
还读出了数组特有的elem
、slice
和len
的值。
// 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)
}
对于方法有个比较特殊的地方——方法的第一个参数是自己,这点和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) {}
,结果会是什么样呢?
明白了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
是一个超级简单的结构体,简单到只有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分配在堆上了。
先看个例子:
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
反射