深度剖析Reflect + 實戰案例

nanjingfm發表於2021-03-07

關鍵詞:go、reflect、反射、實戰

Go的反射機制帶來很多動態特性,一定程度上彌補了Go缺少自定義範型而導致的不便利。

Go反射機制設計的目標之一是任何操作(非反射)都可以透過反射機制來完成

變數是由兩部分組成:變數的型別和變數的值。

reflect.Typereflect.Value是反射的兩大基本要素,他們的關係如下:

  • 任意型別都可以轉換成TypeValue
  • Value可以轉換成Type
  • Value可以轉換成Interface

image-20210307122816992

型別系統

Type描述的是變數的型別,關於型別請參考下面這個文章:Go型別系統概述

Go語言的型別系統非常重要,如果不熟知這些概念,則很難精通Go程式設計。

Type是什麼?

reflect.Type實際上是一個介面,它提供很多api(方法)讓你獲取變數的各種資訊。比如對於陣列提供了LenElem兩個方法分別獲取陣列的長度和元素。

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-20210307122909586

每種型別可以使用的方法都是不一樣的,錯誤的使用會引發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是命名變數
  • hash:型別的hash值,每一種型別在runtime裡面都是唯一的
  • kind:底層型別,一定是官方庫定義的26個基本內建型別其中之一
  • equal:確定型別是否可以比較

看到這裡發現rtype型別描述的資訊是有限的,比如一個arraylen是多長,陣列元素的型別,都無法體現。你知道這些問題的答案麼?

看下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)
    ...
}

觀察下arrayTypechanType的定義,第一位都是一個rtype。我們可以簡單理解,就是一塊記憶體空間,最開頭就是rtype,後面根據型別不同跟著的結構也是不同的。(*rtype)(unsafe.Pointer(t))只讀取開頭的rtype(*arrayType)(unsafe.Pointer(t))強制轉換之後,不僅讀出了rtype還讀出了陣列特有的elemslicelen的值。

// 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-20210307122933501

反射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 協議》,轉載必須註明作者和本文連結
您的點贊、評論和關注,是我創作的不懈動力。 學無止境,讓我們一起加油,在技術的衚衕裡越走越深!

相關文章