寫在前面
Go的反射機制帶來很多動態特性,一定程度上彌補了Go缺少自定義范型而導致的不便利,
Go反射機制設計的目標之一是任何操作(非反射)都可以通過反射機制來完成,
變數是由兩部分組成:變數的型別和變數的值,
型別和值
reflect.Type和reflect.Value是反射的兩大基本要素,他們的關系如下:
- 任意型別都可以轉換成
Type和Value Value可以轉換成TypeValue可以轉換成Interface

Type
型別系統
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) {},結果會是什么樣呢?
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
反射
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/264210.html
標籤:區塊鏈
上一篇:2021年最詳細以太坊(ETH)挖礦教程,手把手教會你以太坊挖礦
下一篇:Golang學習之路—map
