反射是 Go 語(yǔ)言學(xué)習(xí)的一個(gè)難點(diǎn),但也是非常重要的一個(gè)知識(shí)點(diǎn)棠众。反射是洞悉 Go 語(yǔ)言類型系統(tǒng)設(shè)計(jì)的法寶,Go 語(yǔ)言的 ORM 庫(kù)離不開(kāi)它,Go 語(yǔ)言的 json 序列化庫(kù)離不開(kāi)它越平,Go 語(yǔ)言的運(yùn)行時(shí)更是離不開(kāi)它。筆者在學(xué)習(xí)反射功能的時(shí)候也是費(fèi)了好大一番功夫才敢說(shuō)自己確實(shí)搞懂了灵迫。下面請(qǐng)讀者跟著我的步伐來(lái)一步一步深入理解反射功能秦叛。
反射的目標(biāo)
反射的目標(biāo)之一是獲取變量的類型信息,例如這個(gè)類型的名稱瀑粥、占用字節(jié)數(shù)挣跋、所有的方法列表、所有的內(nèi)部字段結(jié)構(gòu)狞换、它的底層存儲(chǔ)類型等等避咆。
反射的目標(biāo)之二是動(dòng)態(tài)的修改變量?jī)?nèi)部字段值。比如 json 的反序列化修噪,你有的是對(duì)象內(nèi)部字段的名稱和相應(yīng)的值查库,你需要把這些字段的值循環(huán)填充到對(duì)象相應(yīng)的字段里。
reflect.Kind
reflect 包定義了十幾種內(nèi)置的「元類型」黄琼,每一種元類型都有一個(gè)整數(shù)編號(hào)樊销,這個(gè)編號(hào)使用 reflect.Kind 類型表示。不同的結(jié)構(gòu)體是不同的類型脏款,但是它們都是同一個(gè)元類型 Struct围苫。包含不同子元素的切片也是不同的類型,但是它們都會(huì)同一個(gè)元類型 Slice撤师。
type Kind uint
const (
Invalid Kind = iota // 不存在的無(wú)效類型
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr // 指針的整數(shù)類型剂府,對(duì)指針進(jìn)行整數(shù)運(yùn)算時(shí)使用
Float32
Float64
Complex64
Complex128
Array // 數(shù)組類型
Chan // 通道類型
Func // 函數(shù)類型
Interface // 接口類型
Map // 字典類型
Ptr // 指針類型
Slice // 切片類型
String // 字符串類型
Struct // 結(jié)構(gòu)體類型
UnsafePointer // unsafe.Pointer 類型
)
反射的基礎(chǔ)代碼
reflect 包提供了兩個(gè)基礎(chǔ)反射方法,分別是 TypeOf() 和 ValueOf() 方法丈氓,分別用于獲取變量的類型和值周循,定義如下
func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value
下面是一個(gè)簡(jiǎn)單的例子,對(duì)結(jié)構(gòu)體變量進(jìn)行反射
package main
import "fmt"
import "reflect"
func main() {
var s int = 42
fmt.Println(reflect.TypeOf(s))
fmt.Println(reflect.ValueOf(s))
}
--------
int
42
這兩個(gè)方法的參數(shù)是 interface{} 類型万俗,意味著調(diào)用時(shí)編譯器首先會(huì)將目標(biāo)變量轉(zhuǎn)換成 interface{} 類型湾笛。在接口小節(jié)我們提到接口類型包含兩個(gè)指針,一個(gè)指向類型闰歪,一個(gè)指向值嚎研,上面兩個(gè)方法的作用就是將接口變量進(jìn)行解剖分離出類型和值。
[圖片上傳中...(image-4463a2-1562577649641-1)]
TypeOf() 方法返回變量的類型信息得到的是一個(gè)類型為 reflect.Type 的變量,ValueOf() 方法返回變量的值信息得到的是一個(gè)類型為 reflect.Value 的變量临扮。
reflect.Type
它是一個(gè)接口類型论矾,里面定義了非常多的方法用于獲取和這個(gè)類型相關(guān)的一切信息。這個(gè)接口的結(jié)構(gòu)體實(shí)現(xiàn)隱藏在 reflect 包里杆勇,每一種類型都有一個(gè)相關(guān)的類型結(jié)構(gòu)體來(lái)表達(dá)它的結(jié)構(gòu)信息贪壳。
type Type interface {
...
Method(i int) Method // 獲取掛在類型上的第 i'th 個(gè)方法
...
NumMethod() int // 該類型上總共掛了幾個(gè)方法
Name() string // 類型的名稱
PkgPath() string // 所在包的名稱
Size() uintptr // 占用字節(jié)數(shù)
String() string // 該類型的字符串形式
Kind() Kind // 元類型
...
Bits() // 占用多少位
ChanDir() // 通道的方向
...
Elem() Type // 數(shù)組,切片蚜退,通道闰靴,指針,字典(key)的內(nèi)部子元素類型
Field(i int) StructField // 獲取結(jié)構(gòu)體的第 i'th 個(gè)字段
...
In(i int) Type // 獲取函數(shù)第 i'th 個(gè)參數(shù)類型
Key() Type // 字典的 key 類型
Len() int // 數(shù)組的長(zhǎng)度
NumIn() int // 函數(shù)的參數(shù)個(gè)數(shù)
NumOut() int // 函數(shù)的返回值個(gè)數(shù)
Out(i int) Type // 獲取函數(shù) 第 i'th 個(gè)返回值類型
common() *rtype // 獲取類型結(jié)構(gòu)體的共同部分
uncommon() *uncommonType // 獲取類型結(jié)構(gòu)體的不同部分
}
所有的類型結(jié)構(gòu)體都包含一個(gè)共同的部分信息钻注,這部分信息使用 rtype 結(jié)構(gòu)體描述蚂且,rtype 實(shí)現(xiàn)了 Type 接口的所有方法。剩下的不同的部分信息各種特殊類型結(jié)構(gòu)體都不一樣幅恋⌒铀溃可以將 rtype 理解成父類,特殊類型的結(jié)構(gòu)體是子類捆交,會(huì)有一些不一樣的字段信息淑翼。
// 基礎(chǔ)類型 rtype 實(shí)現(xiàn)了 Type 接口
type rtype struct {
size uintptr // 占用字節(jié)數(shù)
ptrdata uintptr
hash uint32 // 類型的hash值
...
kind uint8 // 元類型
...
}
// 切片類型
type sliceType struct {
rtype
elem *rtype // 元素類型
}
// 結(jié)構(gòu)體類型
type structType struct {
rtype
pkgPath name // 所在包名
fields []structField // 字段列表
}
...
reflect.Value
不同于 reflect.Type 接口,reflect.Value 是結(jié)構(gòu)體類型品追,一個(gè)非常簡(jiǎn)單的結(jié)構(gòu)體窒舟。
type Value struct {
typ *rtype // 變量的類型結(jié)構(gòu)體
ptr unsafe.Pointer // 數(shù)據(jù)指針
flag uintptr // 標(biāo)志位
}
這個(gè)接口體包含變量的類型結(jié)構(gòu)體指針、數(shù)據(jù)的地址指針和一些標(biāo)志位信息诵盼。里面的類型結(jié)構(gòu)體指針字段就是上面的 rtype 結(jié)構(gòu)體地址,存儲(chǔ)了變量的類型信息银还。標(biāo)志位里有幾個(gè)位存儲(chǔ)了值的「元類型」风宁。下面我們看個(gè)簡(jiǎn)單的例子
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() 等價(jià)于 reflect.TypeOf(s)
fmt.Println(t == v.Type())
fmt.Println(v.Kind() == reflect.Int) // 元類型
// 將 Value 還原成原來(lái)的變量
var is = v.Interface()
fmt.Println(is.(SomeInt))
}
----------
true
true
42
Value 結(jié)構(gòu)體的 Type() 方法也可以返回變量的類型信息,它可以作為 reflect.TypeOf() 函數(shù)的替代品蛹疯,沒(méi)有區(qū)別戒财。通過(guò) Value 結(jié)構(gòu)體提供的 Interface() 方法可以將 Value 還原成原來(lái)的變量值。
將上面的各種關(guān)系整理一下捺弦,可以得到下面這張圖
Value 這個(gè)結(jié)構(gòu)體雖然很簡(jiǎn)單饮寞,但是附著在 Value 上的方法非常之多,主要是用來(lái)方便用戶讀寫(xiě) ptr 字段指向的數(shù)據(jù)內(nèi)存列吼。雖然我們也可以通過(guò) unsafe 包來(lái)精細(xì)操控內(nèi)存幽崩,但是使用過(guò)于繁瑣,使用 Value 結(jié)構(gòu)體提供的方法會(huì)更加簡(jiǎn)單直接寞钥。
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) // 向通道發(fā)送一個(gè)值
func (v Value) Recv() (x Value, ok bool) // 從通道接受一個(gè)值
// Send 和 Recv 的非阻塞版本
func (v Value) TryRecv() (x Value, ok bool)
func (v Value) TrySend(x Value) bool
// 獲取切片慌申、字符串、數(shù)組的具體位置的值進(jìn)行讀寫(xiě)
func (v Value) Index(i int) Value
// 根據(jù)名稱獲取結(jié)構(gòu)體的內(nèi)部字段值進(jìn)行讀寫(xiě)
func (v Value) FieldByName(name string) Value
// 將接口變量裝成數(shù)組理郑,一個(gè)是類型指針蹄溉,一個(gè)是數(shù)據(jù)指針
func (v Value) InterfaceData() [2]uintptr
// 根據(jù)名稱獲取結(jié)構(gòu)體的方法進(jìn)行調(diào)用
// Value 結(jié)構(gòu)體的數(shù)據(jù)指針 ptr 可以指向方法體
func (v Value) MethodByName(name string) Value
...
值得注意的是咨油,觀察 Value 結(jié)構(gòu)體提供的很多方法,其中有不少會(huì)返回 Value 類型柒爵。比如反射數(shù)組類型的 Index(i int) 方法役电,它會(huì)返回一個(gè)新的 Value 對(duì)象,這個(gè)對(duì)象的類型指向數(shù)組內(nèi)部子元素的類型棉胀,對(duì)象的數(shù)據(jù)指針會(huì)指向數(shù)組指定位置子元素所在的內(nèi)存法瑟。
理解 Go 語(yǔ)言官方的反射三大定律
官方對(duì) Go 語(yǔ)言的反射功能做了一個(gè)抽象的描述,總結(jié)出了三大定律膏蚓,分別是
Reflection goes from interface value to reflection object.
Reflection goes from reflection object to interface value.
To modify a reflection object, the value must be settable.
第一個(gè)定律的意思是反射將接口變量轉(zhuǎn)換成反射對(duì)象 Type 和 Value瓢谢,這個(gè)很好理解,就是下面這兩個(gè)方法的功能
func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value
第二個(gè)定律的意思是反射可以通過(guò)反射對(duì)象 Value 還原成原先的接口變量驮瞧,這個(gè)指的就是 Value 結(jié)構(gòu)體提供的 Interface() 方法氓扛。注意它得到的是一個(gè)接口變量,如果要換成成原先的變量還需要經(jīng)過(guò)一次造型论笔。
func (v Value) Interface() interface{}
前兩個(gè)定律比較簡(jiǎn)單采郎,它的意思可以使用前面畫(huà)的反射關(guān)系圖來(lái)表達(dá)。第三個(gè)定律的功能不是很好理解狂魔,它的意思是想用反射功能來(lái)修改一個(gè)變量的值蒜埋,前提是這個(gè)值可以被修改。
值類型的變量是不可以通過(guò)反射來(lái)修改最楷,因?yàn)樵诜瓷渲罢荩瑐鲄⒌臅r(shí)候需要將值變量轉(zhuǎn)換成接口變量,值內(nèi)容會(huì)被淺拷貝籽孙,反射對(duì)象 Value 指向的數(shù)據(jù)內(nèi)存地址不是原變量的內(nèi)存地址烈评,而是拷貝后的內(nèi)存地址。這意味著如果值類型變量可以通過(guò)反射功能來(lái)修改犯建,那么修改操作根本不會(huì)影響到原變量的值讲冠,那就白白修改了。所以 reflect 包就直接禁止了通過(guò)反射來(lái)修改值類型的變量适瓦。我們看個(gè)例子
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
嘗試通過(guò)反射來(lái)修改整型變量失敗了竿开,程序直接拋出了異常。下面我們來(lái)嘗試通過(guò)反射來(lái)修改指針變量指向的值玻熙,這個(gè)是可行的否彩。
package main
import "fmt"
import "reflect"
func main() {
var s int = 42
// 反射指針類型
var v = reflect.ValueOf(&s)
// 要拿出指針指向的元素進(jìn)行修改
v.Elem().SetInt(43)
fmt.Println(s)
}
-------
43
可以看到變量 s 的值確實(shí)被修改成功了,不過(guò)這個(gè)例子修改的是指針指向的值而不是修改指針變量本身嗦随,如果不使用 Elem() 方法進(jìn)行修改也會(huì)拋出一樣的異常胳搞。
結(jié)構(gòu)體也是值類型,也必須通過(guò)指針類型來(lái)修改。下面我們嘗試使用反射來(lái)動(dòng)態(tài)修改結(jié)構(gòu)體內(nèi)部字段的值肌毅。
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}
反射的基礎(chǔ)功能就介紹到這里筷转,在本書(shū)的高級(jí)部分,我們將通過(guò)反射功能完成一個(gè)簡(jiǎn)單的 ORM 框架悬而,這個(gè)大作業(yè)非常有挑戰(zhàn)性呜舒,讀者們先把基礎(chǔ)打牢才可以嘗試。