關鍵詞:go、reflect、反射、實戰
Go的反射機制帶來很多動態特性,一定程度上彌補了Go缺少自定義範型而導致的不便利。
Go反射機制設計的目標之一是任何操作(非反射)都可以透過反射機制來完成。
變數是由兩部分組成:變數的型別和變數的值。
reflect.Type
和reflect.Value
是反射的兩大基本要素,他們的關係如下:
- 任意型別都可以轉換成
Type
和Value
Value
可以轉換成Type
Value
可以轉換成Interface
型別系統
Type
描述的是變數的型別,關於型別請參考下面這個文章:Go型別系統概述
Go語言的型別系統非常重要,如果不熟知這些概念,則很難精通Go程式設計。
Type是什麼?
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
不支援?
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是命名變數
- tflagUncommon: 是否包含一個指標,比如
- hash:型別的hash值,每一種型別在runtime裡面都是唯一的
- kind:底層型別,一定是官方庫定義的26個基本內建型別其中之一
- equal:確定型別是否可以比較
- …
看到這裡發現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)
}
反射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) {}
,結果會是什麼樣呢?
明白了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 協議》,轉載必須註明作者和本文連結