日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Go?reflect?反射原理示例詳解_Golang

作者:飛天薯條 ? 更新時間: 2022-12-05 編程語言

開始之前

在開始分析原理之前,有必要問一下自己一個問題:

反射是什么?以及其作用是什么?

不論在哪種語言中,我們所提到的反射功能,均指開發者可以在運行時通過調用反射庫來獲取到來獲取到指定對象類型信息,通常類型信息中會包含對象的字段/方法等信息。并且,反射庫通常會提供方法的調用, 以及字段賦值等功能。

使用反射可以幫助我們避免寫大量重復的代碼, 因此反射功能常見用于ORM框架, 以及序列化何反序列化框架,除此之外在Java中反射還被應用到了AOP等功能中。

了解完反射的功能之后,我們再引申一個問題:

假如你開發了一種語言, 該如何為開發者提供反射的功能?

首先,我們知道反射的核心的功能有:

  • 類型信息獲取
  • 對象字段訪問/賦值
  • 方法調用

因此實際作為語言的開發者(假設),我們要解決的問題有:

  • 如何存儲并獲取到對象類型信息?
  • 如何定位到對象字段的內存地址?

注: 只要知道了對象字段的內存地址配合上類型信息,我們便可以實現賦值與訪問的操作。

  • 如何定位到方法的內存地址?

注:代碼在內存中也是數據,因此只需要定位到代碼所在的地址,便可解決方法調用的問題

分析

從何處獲取類型信息

如果你熟悉Go的reflect(反射)庫, 相信你或多或少的聽過反射三原則, 即:

  • interface{}可以反射出反射對象
  • 從反射對象中可以獲取到interface{}
  • 要修改反射對象, 其值必須可設置

根據以上三原則不難看出interface{}是實現反射功能的基石, 那么這是為什么呢?

要回答這個問題,我們了解interface{}的本質是什么。

interface{}本質上Go提供的一種數據類型, 與其他數據類型不同的是, interface{}會為我們提供變量的類型信息以及變量所在的內存地址。

Runtime中使用結構體來表示interface{}, 其結構如下所示:

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer  
}

該結構體只有兩個字段, 分別是:

  • typ 變量的類型信息, 這一步驟在編譯步驟便可確定下來
  • word 指向變量數據的指針, 這一步驟在運行時進行確定

接下來我們通過反編譯下文的代碼, 來觀察當把一個變量轉換成interface{}的時候都發生了什么:

package main
import "fmt"
func main() {
	s := 1024
	var a interface{} = &s
	fmt.Println(a)
}

執行以下命令, 獲取匯編代碼

go tool compile -N -S .\main.go

以下代碼即為將字符串賦值給interface{}類型的變量a的對應匯編代碼

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)

相信即便你不熟悉匯編,但至少也發現了, 以上代碼做了如下操作:

  • 獲取變量s的地址, 保存到AX寄存器, 并往a+144的地址寫入數據
  • 獲取變量s的類型信息(type.*int),保存到CX寄存器, 并往a+152的地址寫入數據

注:感興趣的讀者可以把取地址的操作去掉,再看看有什么不同

此外, 我們還可以通過指針數據類型轉換來獲取到interface{}中的數據來側面驗證一下。

注: unsafe.Pointer 可以轉換成任意類型的指針

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
	//此處由編譯器做了類型轉換 *string -> interface{}
	wordPtr := getWordPtr(strPtr)
	t.Logf("String Ptr: %p",  strPtr)
	t.Logf("Word Ptr: %p", wordPtr)
}

輸入如下所示:

因此,不難推出reflect.TypeOf的實現實際上就是獲取interface{}type信息,并返回給開發人員。其代碼如下所示:

func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}
// 將 *rtype 轉成接口類型的Type
func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}

再進一步我們可以來看看類型信息中都包含了什么?

結構體rtype描述了基礎的類型信息,其字段如下所示:

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結構體包含了Golang中所有數據類型的基礎類型信息, 對于不同的數據類型其類型信息會有略微的差異。

// 結構體的類型信息
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)
}

如何實現賦值操作?

賦值操作的本質上是往對應的內存地址寫入數據, 因此我們有必要簡單了解一下結構體在內存中的布局方式, 以一個最為簡單坐標的結構體為例,其結構體如下所示:

type Coordinate struct {
    X int64
    Y int64
    Z int64
}

其在內存中的表現為一段大小為24字節的連續內存,具體如下圖所示

因此,我們實際上要做的就是獲取到結構體的首地址之后,根據各個字段相對首字段的偏移地址計算出其在內存中地址。

實際上在Runtime提供的類型信息中,已經包含了各個字段的偏移以及類型信息,我們可以具體的來看一下反射功能獲取字段Field的實現。

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
	// 繼承結構體的部分flag信息
	fl := v.flag&(flagStickyRO|flagIndir|flagAddr) | flag(typ.Kind())
	if !field.name.isExported() {
		if field.embedded() {
			fl |= flagEmbedRO
		} else {
			fl |= flagStickyRO
		}
	}
	// 根據偏移地址計 + 結構體的首地址 計算出 字段在內存中的地址, 并返回Value對象
	ptr := add(v.ptr, field.offset(), "same as non-reflect &v.field")
	return Value{typ, ptr, fl}
}

了解到如何獲取字段在內存中的地址之后,我們再來看看賦值操作是如何實現。

如以下代碼SetInt所示, 本質上還是一些指針的轉換以及解引用。

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

那么,肯定有同學會問,為啥你一直都在講結構體啊,那字符串(string), 切片(slice), map呢?

實際上這些Go的內建的數據類型,在Runtime中的表現形式也是結構體, 我們可以在reflect包中找到如下定義:

// 切片頭
type SliceHeader struct {
	Data uintptr // 數組的指針地址
	Len  int     // 數組長度
	Cap  int     // 數組容量
}
// 字符串頭
type StringHeader struct {
	Data uintptr // 字節數組的指針地址
	Len  int     // 字節數組的長度
}

因此,通過反射來操作切片和字符串本質上還是操作結構體。

總結

  • interface{}是一種數據類型, 其存儲了變量的類型信息與數據指針,其中類型信息是在編譯期間確定下來的
  • Golang反射的原理就是從interface{}中獲取到類型信息以及變量的指針,從而實現類型獲取以及賦值的功能

原文鏈接:https://juejin.cn/post/7159797055060672543

欄目分類
最近更新