golang reflect 實現原理

eudore發表於2020-04-10

golang reflect 實現原理

本文主要講述 reflect 庫實現的原理思路,reflect 包實現具有兩個基礎 unsafe 操作記憶體對齊和 runtime 包的變數。

runtime 變數

runtime 變數是 reflect 的實現基礎,基於 unsafe 包操作 runtime 變數實現 reflect 功能。

首先我們按照 go 的規則先簡單的定義一個變數型別 Value,Value 有兩個型別成員屬性 typ 和 ptr,typ 是型別表示這個變數是什麼物件,ptr 是一個地址指向這個變數的地址。

// 如果看reflect或runtime原始碼會發現兩者相識,只不過被我刪了不少屬性。
type Value struct {
    typ Type
    ptr uintptr
}

type Type interface {
    Name() string         // by all type
    Index(int) Value      // by Slice Array
    MapIndex(value) Value // by Map
    Send(Value)           // By Chan
}

當我們去操作一個變數時就按照 Type 型別來操作,而操作物件的資料就在記憶體的 ptr 位置。

變數型別 Type 定義的是一個介面,因為不同型別有不同的操作方法,例如 Map 的獲取/設定值,Slice 和 Array 的獲取一個索引,Chan 具有傳送和接實一個物件,Struct 可以獲得一個結構體屬性,屬性具有 tag,這樣不同的型別就具有不同獨特的操作方法,如果 Map 型別呼叫 Index 方法無法實現就會 panic 了。

理解變數本質就是一個資料地址和一個型別資料組成,然後基於者兩個變數來操作就是 reflect。

reflect example

一個 reflect 簡單的例子,reflect.TypeOfreflect.ValueOf方法將一個 runtime 型別和變數轉換成 reflect 型別和變數,依賴 unsafe 操作記憶體對齊來強制轉換,reflect 型別和變數和 runtime 中一樣的,就可以實現自由操作了。

最後reflect.Value呼叫Interface()方法將變數從 reflect 狀態轉換回來成 runtime 狀態了。

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    s := new(Student)
    fmt.Println(reflect.TypeOf(s))
    fmt.Println(reflect.TypeOf(s).Elem())
    fmt.Println(reflect.TypeOf(*s))

    v := reflect.ValueOf(s).Elem()
    v.Field(0).SetString("66")
    fmt.Printf("%#v\n", v.Interface())
}

reflect Type

先從 reflect/type.go 簡單的抄一點程式碼來。rtype 物件就是 Type 介面的簡化實現,kind 就是這個型別的型別,然後其他組合型別 (Ptr、Slice、Map 等) 就額外新增了一些屬性和方法。

type rtype struct {
    size    uintptr
    ptrdata uintptr
    kind    uint8
    ...
}

ptrType 是指標型別的定義,屬性 rtype 就是指標的型別,elem 就是指標指向的型別,那麼一個 Ptr Type 呼叫 Elem 獲得指標的型別就返回了 elem 值。

// ptrType represents a pointer type.
type ptrType struct {
    rtype
    elem *rtype // pointer element (pointed at) type
}

structType 是指標型別的定義,rtype 是結構體型別的基礎資訊,pkgPath 就是結構體的名稱,當一個結構體呼叫 Name 方法時就返回了 pkgPath,如果是結構體指標呼叫 Name 方法就沒有返回資料,因為沒有 pkgPath 需要先 Elem 一次轉換成結構體型別,而結構體型別的 Field、FieldByIndex、FieldByName、FieldByNameFunc 方法就物件結構體型別 fields 資訊進行變數操作了。

而在結構體屬性 structField 中,name、typ 分別記錄這個屬性的名稱和型別,offsetEmbed 是屬性偏移位置。

// structType represents a struct type.
type structType struct {
    rtype
    pkgPath name
    fields  []structField // sorted by offset
}

// Struct field
type structField struct {
    name        name    // name is always non-empty
    typ         *rtype  // type of field
    offsetEmbed uintptr // byte offset of field<<1 | isEmbedded
}

chanType 是 chan 型別的 ing 有,rtype 是 chan 本身,elem 是 chan 操作物件的型別和指標指向相識,dir 就是 chan 的反向進、出、進出。

// chanType represents a channel type.
type chanType struct {
    rtype
    elem *rtype  // channel element type
    dir  uintptr // channel direction (ChanDir)
}

sliceType 是切片型別定義,切片型別 rtype 是本身資訊,elem 就是切片操作的物件型別。

// sliceType represents a slice type.
type sliceType struct {
    rtype
    elem *rtype // slice element type
}

arrayType 是陣列型別,在切片上額外多了兩個屬性,slice 是陣列轉換成切片的型別,預先靜態定義好了,而 len 是陣列長度。

// arrayType represents a fixed array type.
type arrayType struct {
    rtype
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}

上述 example 講述了部分型別的定義,完整檢視原始碼 reflect.type.go。

method、interface、map 暫未完全看完,懂原理後沒必要看沒有幾行使用相關知識。

reflect Kind

reflect.Kind 是定義反射型別常量,是型別的標識。rtype 的 kind 屬性就是指 reflect.Kind。

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

reflect Type method

Kind 方法註釋說明返回 kind 值就是 rtype.kind,型別是 reflect.Kind 是 go 中型別的主要分類,是 iota 定義的型別常量。

// Kind returns the specific kind of this type.
Kind() Kind

變數實現的方法定義在型別連續後面的一塊記憶體中,可以 unsafe 讀到一個型別的全部方法,就可以實現 Implements 方法判斷是否實現了一個介面了。

// Implements reports whether the type implements the interface type u.
Implements(u Type) bool

ChanDir 方法很簡單就返回 chanType.dir,註釋說如果不是 Chan 型別 panic 了,型別不 chan 就沒有 dir 這個屬性無法處理就 panic 了,在呼叫前一般都明確了 Kind 是 Chan。

// ChanDir returns a channel type's direction.
// It panics if the type's Kind is not Chan.
ChanDir() ChanDir

Elem 方法全稱是 element,就是指元素型別也可以叫指向型別,註釋要求 Kind 必須是 Array、Chan、Map、Ptr、Slice 型別否在就 panic,和 Chan 的 ChanDir 方法一樣,只有這 5 個型別才有 elem 屬性。

檢視前面定義就可以知道 Arry、Slice、Ptr、Chan 的 elem 就是指向的物件的型別,map 是值的型別,例如以下型別 Elem 後 Kind 都是 Int。

[20]int
[]int
*int
chan int
map[string]int
// Elem returns a type's element type.
// It panics if the type's Kind is not Array, Chan, Map, Ptr, or Slice.
Elem() Type

Field 和 NumField 方法是獲得結構體的指定索引的屬性和結構體屬性數量,註釋一樣有說明要求 Kind 是 Struct 型別否在 panic,因為就結構體型別才有 [] StructField 能實現這些方法。

根據前面 structType 定義兩個方法的實現思路就是 typ.fields[i] 轉換一下和 len(typ.fields).

// Field returns a struct type's i'th field.
// It panics if the type's Kind is not Struct.
// It panics if i is not in the range [0, NumField()).
Field(i int) StructField
// NumField returns a struct type's field count.
// It panics if the type's Kind is not Struct.
NumField() int

NumIn 和 In 方法是 Func Kind 獨有的方法,NumIn 返回這個 Func 具有多個入參,對於返回引數就是 NumOut;In 方法是獲得這個 Func 指定第 i 引數的型別。

// NumIn returns a function type's input parameter count.
// It panics if the type's Kind is not Func.
NumIn() int
    // In returns the type of a function type's i'th input parameter.
// It panics if the type's Kind is not Func.
// It panics if i is not in the range [0, NumIn()).
In(i int) Type

Key 方法是 Map Kind 獨有方法,返回 map 鍵的型別。


// Key returns a map type's key type.
// It panics if the type's Kind is not Map.
Key() Type

Len 方法是 Array Kind 獨有方法,返回 Array 定義的長度。

// Len returns an array type's length.
// It panics if the type's Kind is not Array.
Len() int

上述說明 reflect.Type 的部分方法實現原理,剩餘方法原理類似,就是操作 rtype 的屬性,部分 Kind 型別是具有獨有方法可以呼叫。

reflect.Value Method

反射 Value 物件定義了三個屬性 型別、資料位置、flag,資料記憶體位置就在 ptr 位置,操作方法就需要依靠 typ 型別來判斷資料型別操作了。

Type 是靜態資料,而 Value 是動態資料,Value 的很多方法具體值是和資料相關的。

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

通用方法

通用方法是指所有型別具有的方法,僅說明根據 Type 和 Value 定義實現這個方法大概的思路,具體實現程式碼並不一樣,以原始碼為準


Type 方法返回這個值的型別,大致思路就是返回 v.typ,具體實現還有一些額外處理。

func (v Value) Type() Type

Kind 方法實現大致思路就是返回 v.typ.kind。

// Kind returns v's Kind. If v is the zero Value (IsValid returns false),
//  Kind returns Invalid.
func (v Value) Kind() Kind

Interface 法思路就是返回 v.ptr 值轉換成一個 interface{}變數,這樣就從 reflect.Value 重新轉換會變數了。

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//  var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing unexported struct fields.
func (v Value) Interface() (i interface{})

Convert 方法思路就是 v.ptr 值轉換成引數 t 的型別,實現規則是Conversions 語法文件 映象地址

// Convert returns the value v converted to type t. If the usual Go conversion rules do not allow conversion of the value v to type t, Convert panics.
func (v Value) Convert(t Type) Value

Set 方法實現就是設定 v.ptr=x.ptr,要求 v 和 x 的型別是一樣的。

同時要這個 Value 是 CanSet,如果將一個 int 轉換成 reflect.Value,函式傳遞的是一個值的副本,那麼再對 int 設定新的值就無效了,CanSet 返回就是 false,需要傳遞*int 這樣的指標型別才能有效設定

// Set assigns x to the value v. It panics if CanSet returns false. As in Go, x's value must be assignable to v's type.
func (v Value) Set(x Value)

SetBool 方法是設定 bool Kind 的值,前置要求 Kind 是一樣的,型別還有 SetInt、SetString 等方法。

// SetBool sets v's underlying value. It panics if v's Kind is not Bool or if CanSet() is false.
func (v Value) SetBool(x bool)

Method 返回這個值的指定索引方法。

// Method returns a function value corresponding to v's i'th method.
// The arguments to a Call on the returned function should not include
// a receiver; the returned function will always use v as the receiver.
// Method panics if i is out of range or if v is a nil interface value.
func (v Value) Method(i int) Value

獨有方法

Len 方法返回資料資料,註釋說明要求是 Array, Chan, Map, Slice, or String,前四個返回就是資料量,而 String Kind 返回字串長度。

// It panics if v's Kind is not Array, Chan, Map, Slice, or String.
func (v Value) Len() int

IsNil 方法判斷指標是否是空,在 go 的實現中 chan、func、interface、map、pointer、slice 底層才是指標型別,才能判斷 IsNil 否在 panic,判斷這些指標型別的 ptr 是否為 0,在 go 程式碼編寫中也只有這幾種型別可以i==nil這樣的比較。

在 go1.13 中新增了 IsZero 方法,判斷是否是空值,裡面這些指標型別會判斷 IsNil,其他型別就是判斷資料值是不是零值那樣。

// IsNil reports whether its argument v is nil. The argument must be
// a chan, func, interface, map, pointer, or slice value; if it is
// not, IsNil panics. Note that IsNil is not always equivalent to a
// regular comparison with nil in Go. For example, if v was created
// by calling ValueOf with an uninitialized interface variable i,
// i==nil will be true but v.IsNil will panic as v will be the zero Value.
func (v Value) IsNil() bool

Index 方法獲取指定型別的索引,就 Array、Slice、String 可以執行,否在 panic,在 ptr 指向的位置進行一個計算得到的偏移位置獲得到索引的值。

// Index returns v's i'th element. It panics if v's Kind is not Array, Slice, or String or i is out of range.
func (v Value) Index(i int) Value

Field 方法是返回結構體指定索引的值,要求 Kind 是 Struct,通過指定索引的偏移來獲得這個值的地址,然後型別裡面獲得到型別,最後返回索引的值。

// Field returns the i'th field of the struct v. It panics if v's Kind is not Struct or i is out of range.
func (v Value) Field(i int) Value

Elem 方法是返回 Ptr 和 Interface Kind 指向值,為了解除引用。

為什麼 Value.Elem 方法沒有了 Slice、Map 等型別? 具體額外獨立的操作方法 Index、MapIndex 等。

// Elem returns the value that the interface v contains or that the pointer v points to. 
// It panics if v's Kind is not Interface or Ptr. It returns the zero Value if v is nil.
func (v Value) Elem() Value

MapIndex 和 MapKeys 是 Map Kind 獨有的方法,獲取到 map 的索引值和全部鍵,通過 typ 提供的型別和 ptr 地址進行復雜的 map 操作。

// MapIndex returns the value associated with key in the map v.
// It panics if v's Kind is not Map.
// It returns the zero Value if key is not found in the map or if v represents a nil map.
// As in Go, the key's value must be assignable to the map's key type.
func (v Value) MapIndex(key Value) Value
// MapKeys returns a slice containing all the keys present in the map,
// in unspecified order.
// It panics if v's Kind is not Map.
// It returns an empty slice if v represents a nil map.
func (v Value) MapKeys() []Value

Send 方法是 Chan Kind 獨有方法,給 chan 放一個資料進去。

func (v Value) Send(x Value)

end

以上講述了 reflect 庫的原理就是操作 runtime 變數,而 runtime 變數就是一個型別加地址。

本文並沒有完整分析 reflect 庫,通過這些原理就可以大概理解這些方法的作用和操作了,具體請參考原始碼。

更多原創文章乾貨分享,請關注公眾號
  • golang reflect 實現原理
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章