《快學 Go 語言》第 15 課 —— 反射

老錢發表於2018-12-24

反射是 Go 語言學習的一個難點,但也是非常重要的一個知識點。反射是洞悉 Go 語言型別系統設計的法寶,Go 語言的 ORM 庫離不開它,Go 語言的 json 序列化庫離不開它,Go 語言的執行時更是離不開它。筆者在學習反射功能的時候也是費了好大一番功夫才敢說自己確實搞懂了。下面請讀者跟著我的步伐來一步一步深入理解反射功能。

反射的目標

反射的目標之一是獲取變數的型別資訊,例如這個型別的名稱、佔用位元組數、所有的方法列表、所有的內部欄位結構、它的底層儲存型別等等。

反射的目標之二是動態的修改變數內部欄位值。比如 json 的反序列化,你有的是物件內部欄位的名稱和相應的值,你需要把這些欄位的值迴圈填充到物件相應的欄位裡。

reflect.Kind

reflect 包定義了十幾種內建的「元型別」,每一種元型別都有一個整數編號,這個編號使用 reflect.Kind 型別表示。不同的結構體是不同的型別,但是它們都是同一個元型別 Struct。包含不同子元素的切片也是不同的型別,但是它們都會同一個元型別 Slice。

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 // unsafe.Pointer 型別
)
複製程式碼

反射的基礎程式碼

reflect 包提供了兩個基礎反射方法,分別是 TypeOf() 和 ValueOf() 方法,分別用於獲取變數的型別和值,定義如下

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value
複製程式碼

下面是一個簡單的例子,對結構體變數進行反射

package main

import "fmt"
import "reflect"

func main() {
	var s int = 42
	fmt.Println(reflect.TypeOf(s))
	fmt.Println(reflect.ValueOf(s))
}

--------
int
42
複製程式碼

這兩個方法的引數是 interface{} 型別,意味著呼叫時編譯器首先會將目標變數轉換成 interface{} 型別。在介面小節我們提到介面型別包含兩個指標,一個指向型別,一個指向值,上面兩個方法的作用就是將介面變數進行解剖分離出型別和值。

《快學 Go 語言》第 15 課 —— 反射
TypeOf() 方法返回變數的型別資訊得到的是一個型別為 reflect.Type 的變數,ValueOf() 方法返回變數的值資訊得到的是一個型別為 reflect.Value 的變數。

reflect.Type

它是一個介面型別,裡面定義了非常多的方法用於獲取和這個型別相關的一切資訊。這個介面的結構體實現隱藏在 reflect 包裡,每一種型別都有一個相關的型別結構體來表達它的結構資訊。

type Type interface {
  ...
  Method(i int) Method  // 獲取掛在型別上的第 i'th 個方法
  ...
  NumMethod() int  // 該型別上總共掛了幾個方法
  Name() string // 型別的名稱
  PkgPath() string // 所在包的名稱
  Size() uintptr // 佔用位元組數
  String() string // 該型別的字串形式
  Kind() Kind // 元型別
  ...
  Bits() // 佔用多少位
  ChanDir() // 通道的方向
  ...
  Elem() Type // 陣列,切片,通道,指標,字典(key)的內部子元素型別
  Field(i int) StructField // 獲取結構體的第 i'th 個欄位
  ...
  In(i int) Type  // 獲取函式第 i'th 個引數型別
  Key() Type // 字典的 key 型別
  Len() int // 陣列的長度
  NumIn() int // 函式的引數個數
  NumOut() int // 函式的返回值個數
  Out(i int) Type // 獲取函式 第 i'th 個返回值型別
  common() *rtype // 獲取型別結構體的共同部分
  uncommon() *uncommonType // 獲取型別結構體的不同部分
}
複製程式碼

所有的型別結構體都包含一個共同的部分資訊,這部分資訊使用 rtype 結構體描述,rtype 實現了 Type 介面的所有方法。剩下的不同的部分資訊各種特殊型別結構體都不一樣。可以將 rtype 理解成父類,特殊型別的結構體是子類,會有一些不一樣的欄位資訊。

// 基礎型別 rtype 實現了 Type 介面
type rtype struct {
  size uintptr // 佔用位元組數
  ptrdata uintptr
  hash uint32 // 型別的hash值
  ...
  kind uint8 // 元型別
  ...
}

// 切片型別
type sliceType struct {
  rtype
  elem *rtype // 元素型別
}

// 結構體型別
type structType struct {
  rtype
  pkgPath name  // 所在包名
  fields []structField  // 欄位列表
}

...
複製程式碼

reflect.Value

不同於 reflect.Type 介面,reflect.Value 是結構體型別,一個非常簡單的結構體。

type Value struct {
  typ *rtype  // 變數的型別結構體
  ptr unsafe.Pointer // 資料指標
  flag uintptr // 標誌位
}
複製程式碼

這個介面體包含變數的型別結構體指標、資料的地址指標和一些標誌位資訊。裡面的型別結構體指標欄位就是上面的 rtype 結構體地址,儲存了變數的型別資訊。標誌位裡有幾個位儲存了值的「元型別」。下面我們看個簡單的例子

package main

import "reflect"
import "fmt"

func main() {
	type SomeInt int
	var s SomeInt = 42
	var t = reflect.TypeOf(s)
	var v = reflect.ValueOf(s)
	// reflect.ValueOf(s).Type() 等價於 reflect.TypeOf(s)
	fmt.Println(t == v.Type())
	fmt.Println(v.Kind() == reflect.Int) // 元型別
	// 將 Value 還原成原來的變數
	var is = v.Interface()
	fmt.Println(is.(SomeInt))
}

----------
true
true
42
複製程式碼

Value 結構體的 Type() 方法也可以返回變數的型別資訊,它可以作為 reflect.TypeOf() 函式的替代品,沒有區別。通過 Value 結構體提供的 Interface() 方法可以將 Value 還原成原來的變數值。

將上面的各種關係整理一下,可以得到下面這張圖

《快學 Go 語言》第 15 課 —— 反射
Value 這個結構體雖然很簡單,但是附著在 Value 上的方法非常之多,主要是用來方便使用者讀寫 ptr 欄位指向的資料記憶體。雖然我們也可以通過 unsafe 包來精細操控記憶體,但是使用過於繁瑣,使用 Value 結構體提供的方法會更加簡單直接。

 func (v Value) SetLen(n int)  // 修改切片的 len 屬性
 func (v Value) SetCap(n int) // 修改切片的 cap 屬性
 func (v Value) SetMapIndex(key, val Value) // 修改字典 kv
 func (v Value) Send(x Value) // 向通道傳送一個值
 func (v Value) Recv() (x Value, ok bool) // 從通道接受一個值
 // Send 和 Recv 的非阻塞版本
 func (v Value) TryRecv() (x Value, ok bool)
 func (v Value) TrySend(x Value) bool
 
 // 獲取切片、字串、陣列的具體位置的值進行讀寫
 func (v Value) Index(i int) Value
 // 根據名稱獲取結構體的內部欄位值進行讀寫
 func (v Value) FieldByName(name string) Value
 // 將介面變數裝成陣列,一個是型別指標,一個是資料指標
 func (v Value) InterfaceData() [2]uintptr
 // 根據名稱獲取結構體的方法進行呼叫
 // Value 結構體的資料指標 ptr 可以指向方法體
 func (v Value) MethodByName(name string) Value
 ...
複製程式碼

值得注意的是,觀察 Value 結構體提供的很多方法,其中有不少會返回 Value 型別。比如反射陣列型別的 Index(i int) 方法,它會返回一個新的 Value 物件,這個物件的型別指向陣列內部子元素的型別,物件的資料指標會指向陣列指定位置子元素所在的記憶體。

理解 Go 語言官方的反射三大定律

官方對 Go 語言的反射功能做了一個抽象的描述,總結出了三大定律,分別是

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一個定律的意思是反射將介面變數轉換成反射物件 Type 和 Value,這個很好理解,就是下面這兩個方法的功能

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value
複製程式碼

第二個定律的意思是反射可以通過反射物件 Value 還原成原先的介面變數,這個指的就是 Value 結構體提供的 Interface() 方法。注意它得到的是一個介面變數,如果要換成成原先的變數還需要經過一次造型。

func (v Value) Interface() interface{}
複製程式碼

前兩個定律比較簡單,它的意思可以使用前面畫的反射關係圖來表達。第三個定律的功能不是很好理解,它的意思是想用反射功能來修改一個變數的值,前提是這個值可以被修改。

值型別的變數是不可以通過反射來修改,因為在反射之前,傳參的時候需要將值變數轉換成介面變數,值內容會被淺拷貝,反射物件 Value 指向的資料記憶體地址不是原變數的記憶體地址,而是拷貝後的記憶體地址。這意味著如果值型別變數可以通過反射功能來修改,那麼修改操作根本不會影響到原變數的值,那就白白修改了。所以 reflect 包就直接禁止了通過反射來修改值型別的變數。我們看個例子

package main

import "reflect"

func main() {
	var s int = 42
	var v = reflect.ValueOf(s)
	v.SetInt(43)
}

---------
panic: reflect: reflect.Value.SetInt using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignable(0x82)
	/usr/local/go/src/reflect/value.go:234 +0x157
reflect.Value.SetInt(0x107a1a0, 0xc000016098, 0x82, 0x2b)
	/usr/local/go/src/reflect/value.go:1472 +0x2f
main.main()
	/Users/qianwp/go/src/github.com/pyloque/practice/main.go:8 +0xc0
exit status 2
複製程式碼

嘗試通過反射來修改整型變數失敗了,程式直接丟擲了異常。下面我們來嘗試通過反射來修改指標變數指向的值,這個是可行的。

package main

import "fmt"
import "reflect"

func main() {
	var s int = 42
	// 反射指標型別
	var v = reflect.ValueOf(&s)
	// 要拿出指標指向的元素進行修改
	v.Elem().SetInt(43)
	fmt.Println(s)
}

-------
43
複製程式碼

可以看到變數 s 的值確實被修改成功了,不過這個例子修改的是指標指向的值而不是修改指標變數本身,如果不使用 Elem() 方法進行修改也會丟擲一樣的異常。

結構體也是值型別,也必須通過指標型別來修改。下面我們嘗試使用反射來動態修改結構體內部欄位的值。

package main

import "fmt"
import "reflect"

type Rect struct {
	Width int
	Height int
}

func SetRectAttr(r *Rect, name string, value int) {
	var v = reflect.ValueOf(r)
	var field = v.Elem().FieldByName(name)
	field.SetInt(int64(value))
}

func main() {
	var r = Rect{50, 100}
	SetRectAttr(&r, "Width", 100)
	SetRectAttr(&r, "Height", 200)
	fmt.Println(r)
}

-----
{100 200}
複製程式碼

反射的基礎功能就介紹到這裡,在本書的高階部分,我們將通過反射功能完成一個簡單的 ORM 框架,這個大作業非常有挑戰性,讀者們先把基礎打牢才可以嘗試。

《快學 Go 語言》第 15 課 —— 反射

掃一掃關注「碼洞」,閱讀更多 Go 語言相關文章

相關文章