浅尝go反射

nanjingfm · · 902 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

# 写在前面 Go的反射机制带来很多动态特性,一定程度上弥补了Go缺少自定义范型而导致的不便利。 Go反射机制设计的目标之一是**任何操作(非反射)都可以通过反射机制来完成**。 变量是由两部分组成:变量的类型和变量的值。 # 类型和值 `reflect.Type`和`reflect.Value`是反射的两大基本要素,他们的关系如下: - 任意类型都可以转换成`Type`和`Value` - `Value`可以转换成`Type` - `Value`可以转换成`Interface` ![image-20210226092816988](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20210226092824.png) # Type ## 类型系统 `Type`描述的是变量的类型,关于类型请参考下面这个文章: [Go类型系统概述](https://gfw.go101.org/article/type-system-overview.html) Go语言的类型系统非常重要,如果不熟知这些概念,则很难精通Go编程。 ## Type是什么? `reflect.Type`实际上是一个接口,它提供很多`api`(方法)让你获取变量的各种信息。比如对于数组提供了`Len`和`Elem`两个方法分别获取数组的长度和元素。 ```go 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-20210226092843752](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20210226092843.png) 每种类型可以使用的方法都是不一样的,错误的使用会引发`panic`。 **思考**:为什么`array`支持`Len`方法,而`slice`不支持? ## Type有哪些实现? 使用`reflect.TypeOf`可以获取变量的`Type` ```go 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`。 ```go // 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`结构体: ```go 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:类型占用空间的大小(大小特指类型的直接部分,什么是直接部分请参考[值部](https://gfw.go101.org/article/value-part.html)) - tflag:标志位 - tflagUncommon: 是否包含一个指针,比如`slice`会引用一个`array` - tflagNamed:是否是命名变量,如`var a = []string`,`[]string`就匿名的,a是命名变量 - hash:类型的hash值,每一种类型在runtime里面都是唯一的 - kind:底层类型,一定是官方库定义的[26个基本内置类型](https://gfw.go101.org/article/basic-types-and-value-literals.html)其中之一 - equal:确定类型是否可以比较 - ... 看到这里发现`rtype`类型描述的信息是有限的,比如一个`array`的`len`是多长,数组元素的类型,都无法体现。你知道这些问题的答案么? 看下`Elem`方法的实现——根据`Kind`的不同,可以再次强制转换类型。 ```go 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`的值。 ```go // 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-20210226092905253](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20210226092905.png) ## 反射struct的方法 对于方法有个比较特殊的地方——方法的第一个参数是自己,这点和C相似。 ```go 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()) } } ``` 输出结果为: ```go f有1个方法 Run方法有2个参数 第0个参数是"main.f" 第1个参数是"string" ``` **思考:**如果我们将Run方法定义为`func (p *f) Run(a string) {}`,结果会是什么样呢? # Value 明白了`Type`之后,`Value`就非常好理解了。直接看下`reflect.ValueOf`的代码: ```go 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`: ```go type Value struct { // 类型元数据 typ *rtype // 值的地址 ptr unsafe.Pointer // 标识位 flag } ``` 看到`Value`中也包含了`*rtype`,这就解释了为什么`reflect.Value`可以直接转换成`reflect.Type`。 ## 堆逃逸 逃逸到堆意味着将值拷贝一份到堆上,这也是反射`慢`的主要原因。 ```go func main() { var a = "xxx" _ = reflect.ValueOf(&a) var b = "xxx2" _ = reflect.TypeOf(&b) } ``` 然后想要看到是否真的逃逸,可以使用`go build -gcflags -m`编译,输出如下: ```go ./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的问题 先看个例子🌰: ```go 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语言选择了报错提醒。 ## 通过反射修改值 既然不能直接传递值,那么就传递变量地址吧! ```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`函数) 代码如下: ```go // 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`,需要使用断言来转换成目标类型。 ```go var phones = []string{"a","b","c"} chunks := SliceChunk(phones, 500).([][]string) ``` # 总结 虽然反射很灵活(几乎可以干任何事情),下面有三点建议: - 可以只使用`reflect.TypeOf`的话,就不要使用`reflect.ValueOf` - 可以使用断言代替的话,就不要使用反射 - 如果有可能应当**避免使用反射** # 参考资料 [The Go Blog](https://blog.golang.org/laws-of-reflection) [反射](https://gfw.go101.org/article/reflection.html) 小白gopher,如有不当,欢迎指正

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

902 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传