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

反射是 Go 語言學習的一個難點,但也是很是重要的一個知識點。反射是洞悉 Go 語言類型系統設計的法寶,Go 語言的 ORM 庫離不開它,Go 語言的 json 序列化庫離不開它,Go 語言的運行時更是離不開它。筆者在學習反射功能的時候也是費了好大一番功夫纔敢說本身確實搞懂了。下面請讀者跟着個人步伐來一步一步深刻理解反射功能。git

反射的目標

反射的目標之一是獲取變量的類型信息,例如這個類型的名稱、佔用字節數、全部的方法列表、全部的內部字段結構、它的底層存儲類型等等。github

反射的目標之二是動態的修改變量內部字段值。好比 json 的反序列化,你有的是對象內部字段的名稱和相應的值,你須要把這些字段的值循環填充到對象相應的字段裏。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() 方法,分別用於獲取變量的類型和值,定義以下bash

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{} 類型。在接口小節咱們提到接口類型包含兩個指針,一個指向類型,一個指向值,上面兩個方法的做用就是將接口變量進行解剖分離出類型和值。 函數

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 理解成父類,特殊類型的結構體是子類,會有一些不同的字段信息。ui

// 基礎類型 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 是結構體類型,一個很是簡單的結構體。spa

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 還原成原來的變量值。

將上面的各類關係整理一下,能夠獲得下面這張圖

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 語言相關文章

相關文章
相關標籤/搜索