Go學(xué)習(xí)筆記-反射

反射是 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)系整理一下捺弦,可以得到下面這張圖

image

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é)出了三大定律膏蚓,分別是

  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.

第一個(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ǔ)打牢才可以嘗試。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笨奠,一起剝皮案震驚了整個(gè)濱河市袭蝗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌般婆,老刑警劉巖到腥,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蔚袍,居然都是意外死亡乡范,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)啤咽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晋辆,“玉大人,你說(shuō)我怎么就攤上這事宇整∑考眩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵鳞青,是天一觀的道長(zhǎng)霸饲。 經(jīng)常有香客問(wèn)我,道長(zhǎng)臂拓,這世上最難降的妖魔是什么贴彼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮埃儿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘融涣。我一直安慰自己童番,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布威鹿。 她就那樣靜靜地躺著剃斧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪忽你。 梳的紋絲不亂的頭發(fā)上幼东,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼根蟹。 笑死脓杉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的简逮。 我是一名探鬼主播球散,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼散庶!你這毒婦竟也來(lái)了蕉堰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悲龟,失蹤者是張志新(化名)和其女友劉穎屋讶,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體须教,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡皿渗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了没卸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羹奉。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖约计,靈堂內(nèi)的尸體忽然破棺而出诀拭,到底是詐尸還是另有隱情,我是刑警寧澤煤蚌,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布耕挨,位于F島的核電站,受9級(jí)特大地震影響尉桩,放射性物質(zhì)發(fā)生泄漏筒占。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一蜘犁、第九天 我趴在偏房一處隱蔽的房頂上張望翰苫。 院中可真熱鬧,春花似錦这橙、人聲如沸奏窑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)埃唯。三九已至,卻和暖如春鹰晨,著一層夾襖步出監(jiān)牢的瞬間墨叛,已是汗流浹背止毕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漠趁,地道東北人扁凛。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像棚潦,于是被迫代替她去往敵國(guó)和親令漂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容