Go reflect 反射原理示例詳解
開始之前
在開始分析原理之前,有必要問一下自己一個問題:
反射是什么?以及其作用是什么?
不論在哪種語言中,我們所提到的反射功能,均指開發(fā)者可以在運(yùn)行時通過調(diào)用反射庫來獲取到來獲取到指定對象類型信息,通常類型信息中會包含對象的字段/方法等信息。并且,反射庫通常會提供方法的調(diào)用, 以及字段賦值等功能。
使用反射可以幫助我們避免寫大量重復(fù)的代碼, 因此反射功能常見用于ORM框架, 以及序列化何反序列化框架,除此之外在Java中反射還被應(yīng)用到了AOP等功能中。
了解完反射的功能之后,我們再引申一個問題:
假如你開發(fā)了一種語言, 該如何為開發(fā)者提供反射的功能?
首先,我們知道反射的核心的功能有:
- 類型信息獲取
- 對象字段訪問/賦值
- 方法調(diào)用
因此實(shí)際作為語言的開發(fā)者(假設(shè)),我們要解決的問題有:
- 如何存儲并獲取到對象類型信息?
- 如何定位到對象字段的內(nèi)存地址?
注: 只要知道了對象字段的內(nèi)存地址配合上類型信息,我們便可以實(shí)現(xiàn)賦值與訪問的操作。
- 如何定位到方法的內(nèi)存地址?
注:代碼在內(nèi)存中也是數(shù)據(jù),因此只需要定位到代碼所在的地址,便可解決方法調(diào)用的問題
分析
從何處獲取類型信息
如果你熟悉Go的reflect(反射)庫, 相信你或多或少的聽過反射三原則, 即:
- 從
interface{}
可以反射出反射對象 - 從反射對象中可以獲取到
interface{}
- 要修改反射對象, 其值必須可設(shè)置
根據(jù)以上三原則不難看出interface{}
是實(shí)現(xiàn)反射功能的基石, 那么這是為什么呢?
要回答這個問題,我們了解interface{}
的本質(zhì)是什么。
interface{}
本質(zhì)上Go提供的一種數(shù)據(jù)類型, 與其他數(shù)據(jù)類型不同的是, interface{}
會為我們提供變量的類型信息以及變量所在的內(nèi)存地址。
在Runtime
中使用結(jié)構(gòu)體來表示interface{}
, 其結(jié)構(gòu)如下所示:
type emptyInterface struct { typ *rtype word unsafe.Pointer }
該結(jié)構(gòu)體只有兩個字段, 分別是:
typ
變量的類型信息, 這一步驟在編譯步驟便可確定下來word
指向變量數(shù)據(jù)的指針, 這一步驟在運(yùn)行時進(jìn)行確定
接下來我們通過反編譯下文的代碼, 來觀察當(dāng)把一個變量轉(zhuǎn)換成interface{}
的時候都發(fā)生了什么:
package main import "fmt" func main() { s := 1024 var a interface{} = &s fmt.Println(a) }
執(zhí)行以下命令, 獲取匯編代碼
go tool compile -N -S .\main.go
以下代碼即為將字符串賦值給interface{}
類型的變量a
的對應(yīng)匯編代碼
0x0057 00087 (.\main.go:7) MOVQ "".&s+104(SP), AX 0x005c 00092 (.\main.go:7) MOVQ AX, ""..autotmp_9+88(SP) 0x0061 00097 (.\main.go:7) LEAQ type.*int(SB), CX 0x0068 00104 (.\main.go:7) MOVQ CX, "".a+144(SP) 0x0070 00112 (.\main.go:7) MOVQ AX, "".a+152(SP)
相信即便你不熟悉匯編,但至少也發(fā)現(xiàn)了, 以上代碼做了如下操作:
- 獲取變量
s
的地址, 保存到AX
寄存器, 并往a+144
的地址寫入數(shù)據(jù) - 獲取變量
s
的類型信息(type.*int
),保存到CX
寄存器, 并往a+152
的地址寫入數(shù)據(jù)
注:感興趣的讀者可以把取地址的操作去掉,再看看有什么不同
此外, 我們還可以通過指針數(shù)據(jù)類型轉(zhuǎn)換來獲取到interface{}
中的數(shù)據(jù)來側(cè)面驗(yàn)證一下。
注: unsafe.Pointer 可以轉(zhuǎn)換成任意類型的指針
type EmptyInterface struct { typ unsafe.Pointer word unsafe.Pointer } func getWordPtr(i interface{}) unsafe.Pointer { eface := *(*EmptyInterface)(unsafe.Pointer(&i)) return eface.word } func Test_GetWordPtr(t *testing.T) { str := "Hello, KeSan" strPtr := &str //此處由編譯器做了類型轉(zhuǎn)換 *string -> interface{} wordPtr := getWordPtr(strPtr) t.Logf("String Ptr: %p", strPtr) t.Logf("Word Ptr: %p", wordPtr) }
輸入如下所示:
因此,不難推出reflect.TypeOf
的實(shí)現(xiàn)實(shí)際上就是獲取interface{}
中type
信息,并返回給開發(fā)人員。其代碼如下所示:
func TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ) } // 將 *rtype 轉(zhuǎn)成接口類型的Type func toType(t *rtype) Type { if t == nil { return nil } return t }
再進(jìn)一步我們可以來看看類型信息中都包含了什么?
結(jié)構(gòu)體rtype
描述了基礎(chǔ)的類型信息,其字段如下所示:
type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero }
rtype
結(jié)構(gòu)體包含了Golang中所有數(shù)據(jù)類型的基礎(chǔ)類型信息, 對于不同的數(shù)據(jù)類型其類型信息會有略微的差異。
// 結(jié)構(gòu)體的類型信息 type structType struct { rtype pkgPath name fields []structField // sorted by offset } // channel 的類型信息 type chanType struct { rtype elem *rtype // channel element type dir uintptr // channel direction (ChanDir) }
如何實(shí)現(xiàn)賦值操作?
賦值操作的本質(zhì)上是往對應(yīng)的內(nèi)存地址寫入數(shù)據(jù), 因此我們有必要簡單了解一下結(jié)構(gòu)體在內(nèi)存中的布局方式, 以一個最為簡單坐標(biāo)的結(jié)構(gòu)體為例,其結(jié)構(gòu)體如下所示:
type Coordinate struct { X int64 Y int64 Z int64 }
其在內(nèi)存中的表現(xiàn)為一段大小為24字節(jié)的連續(xù)內(nèi)存,具體如下圖所示
因此,我們實(shí)際上要做的就是獲取到結(jié)構(gòu)體的首地址之后,根據(jù)各個字段相對首字段的偏移地址計(jì)算出其在內(nèi)存中地址。
實(shí)際上在Runtime
提供的類型信息中,已經(jīng)包含了各個字段的偏移以及類型信息,我們可以具體的來看一下反射功能獲取字段Field
的實(shí)現(xiàn)。
func (v Value) Field(i int) Value { if v.kind() != Struct { panic(&ValueError{"reflect.Value.Field", v.kind()}) } // 獲取類型信息 tt := (*structType)(unsafe.Pointer(v.typ)) if uint(i) >= uint(len(tt.fields)) { panic("reflect: Field index out of range") } // 獲取字段信息 field := &tt.fields[i] typ := field.typ // 繼承結(jié)構(gòu)體的部分flag信息 fl := v.flag&(flagStickyRO|flagIndir|flagAddr) | flag(typ.Kind()) if !field.name.isExported() { if field.embedded() { fl |= flagEmbedRO } else { fl |= flagStickyRO } } // 根據(jù)偏移地址計(jì) + 結(jié)構(gòu)體的首地址 計(jì)算出 字段在內(nèi)存中的地址, 并返回Value對象 ptr := add(v.ptr, field.offset(), "same as non-reflect &v.field") return Value{typ, ptr, fl} }
了解到如何獲取字段在內(nèi)存中的地址之后,我們再來看看賦值操作是如何實(shí)現(xiàn)。
如以下代碼SetInt
所示, 本質(zhì)上還是一些指針的轉(zhuǎn)換以及解引用。
func (v Value) SetInt(x int64) { v.mustBeAssignable() switch k := v.kind(); k { default: panic(&ValueError{"reflect.Value.SetInt", v.kind()}) case Int: *(*int)(v.ptr) = int(x) case Int8: *(*int8)(v.ptr) = int8(x) case Int16: *(*int16)(v.ptr) = int16(x) case Int32: *(*int32)(v.ptr) = int32(x) case Int64: *(*int64)(v.ptr) = x } }
那么,肯定有同學(xué)會問,為啥你一直都在講結(jié)構(gòu)體啊,那字符串(string
), 切片(slice
), map
呢?
實(shí)際上這些Go
的內(nèi)建的數(shù)據(jù)類型,在Runtime
中的表現(xiàn)形式也是結(jié)構(gòu)體, 我們可以在reflect
包中找到如下定義:
// 切片頭 type SliceHeader struct { Data uintptr // 數(shù)組的指針地址 Len int // 數(shù)組長度 Cap int // 數(shù)組容量 } // 字符串頭 type StringHeader struct { Data uintptr // 字節(jié)數(shù)組的指針地址 Len int // 字節(jié)數(shù)組的長度 }
因此,通過反射來操作切片和字符串本質(zhì)上還是操作結(jié)構(gòu)體。
總結(jié)
interface{}
是一種數(shù)據(jù)類型, 其存儲了變量的類型信息與數(shù)據(jù)指針,其中類型信息是在編譯期間確定下來的Golang
反射的原理就是從interface{}
中獲取到類型信息以及變量的指針,從而實(shí)現(xiàn)類型獲取以及賦值的功能
以上就是Go reflect 反射原理示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Go reflect 反射原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)redis的延時消息隊(duì)列功能示例
這篇文章主要介紹了golang實(shí)現(xiàn)redis的延時消息隊(duì)列功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11手把手教你用VS?code快速搭建一個Golang項(xiàng)目
Go語言是采用UTF8編碼的,理論上使用任何文本編輯器都能做Go語言開發(fā),下面這篇文章主要給大家介紹了關(guān)于使用VS?code快速搭建一個Golang項(xiàng)目的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04sublime text3解決Gosublime無法自動補(bǔ)全代碼的問題
本文主要介紹了sublime text3解決Gosublime無法自動補(bǔ)全代碼的問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01Go語言CSP并發(fā)模型實(shí)現(xiàn)MPG
這篇文章主要為大家介紹了Go語言CSP并發(fā)模型實(shí)現(xiàn)MPG圖文詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05