# 写在前面
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.ValueOf`的话,就不要使用`reflect.ValueOf`
- 可以使用断言代替的话,就不要使用反射
- 如果有可能应当**避免使用反射**
# 参考资料
[The Go Blog](https://blog.golang.org/laws-of-reflection)
[反射](https://gfw.go101.org/article/reflection.html)
小白学golang,如有不当,欢迎指正😊。
有疑问加站长微信联系(非本文作者))