從結(jié)構(gòu)體和接口深入理解GO反射

【譯文】原文地址
關(guān)于Go反射這個(gè)主題,需要理解Go內(nèi)部關(guān)于結(jié)構(gòu)體骗炉、接口和類(lèi)型系統(tǒng)照宝,才能理解反射的底層工作機(jī)制。當(dāng)然句葵,您也使用反射厕鹃,而不需要深入理解這些細(xì)節(jié)。本文的目標(biāo)是向您介紹一些細(xì)節(jié)乍丈,使您能夠更深入地理解反射剂碴。但是這些不是嚴(yán)格要求的。這篇文章假設(shè)您對(duì)結(jié)構(gòu)體和接口有基本的了解轻专。你可以通過(guò)"Go by example"快速瀏覽下結(jié)構(gòu)體接口忆矛,也可以深入學(xué)習(xí)下Go的結(jié)構(gòu)體接口

Reflection. Photo by Dawid Zawi?a on Unsplash

什么是反射

In computer science, reflection is the ability of a process to examine, introspect, and modify its own structure and behavior. — Wikipedia

維基百科上面解釋到请垛,在計(jì)算機(jī)科學(xué)中催训,反射是一種能夠?qū)Y(jié)構(gòu)體和行為(本人理解為函數(shù)或者方法)進(jìn)行檢查、內(nèi)省的過(guò)程宗收。
反射是程序運(yùn)行時(shí)的操作漫拭。它是一種元編程形式,但并不是所有的元編程都是反射混稽。

為什么反射對(duì)Go語(yǔ)言重要

反射在很多方面都起作用采驻。通過(guò)本文我將關(guān)注最明顯的一個(gè)作用审胚。Go作為一種靜態(tài)編程語(yǔ)言,你必須提前聲明所有類(lèi)型才可以使用礼旅。因此膳叨,你沒(méi)辦法處理事先不清楚的類(lèi)型,即使你需要對(duì)它進(jìn)行操作各淀、檢查你也不需要了解這些信息懒鉴。

一個(gè)典型的例子就是fmt包中的print函數(shù)。如果你想打印一個(gè)變量的類(lèi)型可以使用%T碎浇,fmt包不需要知道你自定義的Person結(jié)構(gòu)體。但是它還是能打印出Person接頭體內(nèi)容璃俗。

空接口

接口是一種定義了多個(gè)方法的類(lèi)型奴璃,實(shí)現(xiàn)了這些方法的結(jié)構(gòu)體就實(shí)現(xiàn)了該接口。這允許將接口作為一種類(lèi)型傳給方法使用城豁,您可以將實(shí)現(xiàn)了該接口的結(jié)構(gòu)體傳入方法苟穆。對(duì)于一個(gè)空接口,每個(gè)結(jié)構(gòu)體和每個(gè)基本類(lèi)型唱星,內(nèi)建實(shí)現(xiàn)了空接口雳旅。

因此,使用空接口作為類(lèi)型的函數(shù)參數(shù)间聊,可以接受任意類(lèi)型參數(shù)攒盈。

func main() {
  x := 100
  fmt.Println(x+1)
  myPrint(x)
}

func myPrint(item interface{}) {
  fmt.Println(item)
}

上面的代碼可以正常工作,第一個(gè)print將打印101哎榴,因?yàn)閷?duì)類(lèi)型int的變量x執(zhí)行了+1操作型豁,然后將x傳給Println,該函數(shù)也是接收一個(gè)空接口作為參數(shù)尚蝌,內(nèi)部是反射實(shí)現(xiàn)迎变。

但是Go還是一個(gè)靜態(tài)類(lèi)型語(yǔ)言,所以使用空接口將不允許您對(duì)變量進(jìn)行任何其他操作(除非使用類(lèi)型斷言或反射)飘言。

func main() {
  x := 100
  myPrint(x)
}

func myPrint(item interface{}) {
  fmt.Println(item+1)
  fmt.Println(item)
}

上面的代碼無(wú)法編譯通過(guò)衣形,在myPrint函數(shù)中,item是空接口類(lèi)型姿鸿,即使底層是整形谆吴,但是Go并不知道它,因此代碼會(huì)panic般妙。

類(lèi)型斷言

類(lèi)型斷言可以幫助你驗(yàn)證變量的實(shí)際類(lèi)型纪铺,如果它是您斷言的類(lèi)型,就會(huì)以這種類(lèi)型來(lái)獲取對(duì)應(yīng)值碟渺。

func main() {
  var myVar interface{} = 10

  v, ok := myVar.(int)
  if (ok) {
    fmt.Println(v)
  }
}

上面的代碼會(huì)打印10鲜锚,因?yàn)槲覀兪褂胢yVar.(int)得到對(duì)應(yīng)的原始類(lèi)型突诬。斷言成功的話(huà),v將賦值為對(duì)應(yīng)類(lèi)型變量芜繁,ok將賦值為bool值旺隙。

關(guān)于類(lèi)型斷言的更多內(nèi)容,骏令,如果您感興趣的話(huà)可以瀏覽類(lèi)型斷言類(lèi)型切換蔬捷。

類(lèi)型實(shí)現(xiàn)細(xì)節(jié)在哪里呢?

類(lèi)型斷言(以及代理榔袋,反射)是如何知道一個(gè)通用接口(空接口)的底層類(lèi)型的呢周拐。

要理解這點(diǎn),需要通過(guò)/src/sync/atomic/value.go直接看go實(shí)現(xiàn)凰兑。它實(shí)現(xiàn)了go中每個(gè)變量的基礎(chǔ)值妥粟。

// ifaceWords is interface{} internal representation.                       
type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}

空接口和go通過(guò)擴(kuò)展的每個(gè)值,在底層表示中包括兩個(gè)unsafe指針:typ和data吏够。

  • typ保存當(dāng)前變量的類(lèi)型信息勾给,因此即使一個(gè)變量是空接口,實(shí)際的類(lèi)型信息在typ中是完整的锅知。
  • data保存值本身播急,還有其他數(shù)據(jù)信息如kind值,這個(gè)不在本文討論范圍之內(nèi)售睹。重點(diǎn)是data保存類(lèi)型的值信息桩警。

類(lèi)型斷言缺少什么?(或者為何需要反射)

當(dāng)你知道要檢查的類(lèi)型時(shí)侣姆,斷言允許驗(yàn)證和使用接口的底層類(lèi)型值生真。在前面的例子中,我們專(zhuān)門(mén)為int使用斷言捺宗。因?yàn)槲覀兲崆爸榔漕?lèi)型柱蟀,以便斷言可以正常工作。即使我們用switch多個(gè)case來(lái)檢查蚜厉,我們?nèi)匀槐仨氈涝诰幾g時(shí)斷言的具體類(lèi)型长已。

在編譯時(shí)不知道具體的類(lèi)型情況下,就需要反射了昼牛∈跷停或者換句話(huà)說(shuō),如本文開(kāi)頭所述贰健,當(dāng)我們需要在運(yùn)行時(shí)檢查胞四。回想下fmt例子伶椿,fmt包并不知道結(jié)構(gòu)體類(lèi)型但仍然可以打印它的值和類(lèi)型(使用%T)辜伟。

反射

Reflect.Type和Reflect.Value是反射包提供的兩個(gè)基本且最終要的類(lèi)型氓侧。它們是Reflect包中定義的兩個(gè)結(jié)構(gòu)體,reflect包有操作接口變量的方法导狡,底層實(shí)現(xiàn)其實(shí)都是通過(guò)將接口的typ和data信息復(fù)制到這兩個(gè)結(jié)構(gòu)體上约巷。這樣通過(guò)這兩個(gè)結(jié)構(gòu)體對(duì)應(yīng)的方法即可處理接口了。
Reflect.TypeOf()和Reflect.ValueOf()是兩個(gè)可用的基本方法旱捧,分別返回Reflect.Type和Reflect.Value独郎,如下所示:

import (
  "fmt"
  "reflect"
)

func main() {
  var myVar interface{} = 10

  myType := reflect.TypeOf(myVar)
  myValue := reflect.ValueOf(myVar)

  fmt.Println(myType) // > int
  fmt.Println(myValue) // > 10
}

為了更清楚的說(shuō)明:我們可以看一下自己定義的struct例子:


type Person struct {
  name string
}

func main() {
  var myPerson interface{} = Person{name: "Snir David"}

  myType := reflect.TypeOf(myPerson)
  myValue := reflect.ValueOf(myPerson)

  fmt.Println(myType) // > main.Person
  fmt.Println(myValue) // > {Snir David}
}

使用TypeOf和VauleOf返回底層類(lèi)型,和一個(gè)指向值的指針枚赡。需要注意的是ValueOf返回的的值類(lèi)型是Reflect.Value類(lèi)型的氓癌,并不是變量原始類(lèi)型。

例如贫橙,前面的例子中顽铸,我們不能取接口的值并對(duì)其做算數(shù)運(yùn)算,比如myValue + 1料皇。這是無(wú)法通過(guò)編譯的,因?yàn)镚o編譯器無(wú)法根據(jù)Reflect.Value類(lèi)型識(shí)別這個(gè)操作星压,但對(duì)原始類(lèi)型int是可識(shí)別的践剂。

Reflect.Kind

The kind is what the type is made of — a slice, a map, a pointer, a struct, an interface, a string, an array, a function, an int or some other primitive type.

理解Kind是有點(diǎn)棘手的,而且網(wǎng)上的一些介紹也會(huì)讓您感到困惑娜膘,因?yàn)榇蟛糠纸榻Bkind都是根據(jù)type理論逊脯,和討論Haskell之類(lèi)的語(yǔ)言實(shí)現(xiàn)。
關(guān)于Go你需要知道的是竣贪,每個(gè)變量都有一個(gè)Kind類(lèi)型军洼,是從type派生出來(lái)的。Kind可以理解為是類(lèi)型中的類(lèi)型演怎。
最容易說(shuō)清楚kind概念就是自己定義的結(jié)構(gòu)體匕争。讓我們回到前面創(chuàng)建Person結(jié)構(gòu)體的代碼。我們定義的myPerson是一個(gè)Person類(lèi)型爷耀。Person結(jié)構(gòu)體本身類(lèi)型甘桑,即Kind是struct。獲取kind類(lèi)型可以通過(guò)對(duì)Reflect.Type變量使用Kind()方法
例如上面的代碼可以修改為:

 myKind := reflect.TypeOf(myPerson).Kind()

可以在如下鏈接查看所有的Kind值:
https://golang.org/pkg/reflect/#Kind

對(duì)一些例子歹叮,不像上面struct比較明顯跑杭,Kind看起來(lái)似乎有點(diǎn)重復(fù)。例如type是int64其Kind也是int64咆耿。無(wú)需強(qiáng)調(diào)的是德谅,我們了解Kind是因?yàn)樗兄谥赜每战涌谥担ㄗg者:這里似乎沒(méi)解釋清楚)。

將Reflect.Value轉(zhuǎn)換為原始類(lèi)型值

因此我們根據(jù)Reflect.ValueOf()函數(shù)可以對(duì)一個(gè)空接口類(lèi)型變量進(jìn)行分析萨螺,并得到一個(gè)類(lèi)型為Rreflect.Value值呕童。但是這個(gè)值并不正真有用主到,因?yàn)镚o類(lèi)型系統(tǒng)無(wú)法識(shí)別出它的原始類(lèi)型嘴高。

我們希望將該值轉(zhuǎn)換成其原始類(lèi)型。這個(gè)過(guò)程如下:
1冯键、使用Reflect.TypeOf或Reflect.Kind()識(shí)別出其原始類(lèi)型。
2庸汗、使用指向值的指針來(lái)獲取原始值(unsafe指針不在本文范圍內(nèi))
3惫确、對(duì)指針進(jìn)行類(lèi)型轉(zhuǎn)換
很幸運(yùn),reflect包已經(jīng)提供了處理所有的基本類(lèi)型轉(zhuǎn)換函數(shù)蚯舱。如下所示:

func main() {
  var myVar interface{} = 10
  reflectValue := reflect.ValueOf(myVar)
  intValue := reflectValue.Int()
  // Arithmetic will now work, as this is typed int
  fmt.Println(intValue + 1) // > 11
}

所有的基本類(lèi)型都有轉(zhuǎn)換方法可用改化,Bool、Float枉昏、String等陈肛。

復(fù)雜類(lèi)型的探究

上面介紹了基本類(lèi)型,但是我們?nèi)绾翁幚斫Y(jié)構(gòu)體呢兄裂?
reflect包也提供了方法來(lái)查看結(jié)構(gòu)體內(nèi)部信息句旱。如下代碼所示:

type Person struct {
  name string
  age int
}

func investigateStruct(s interface{}) {
  reflValue := reflect.ValueOf(s)
  // Make sure we are handling with a struct here
  if (reflValue.Kind() == reflect.Struct) {
    fieldCount := reflValue.NumField()
    fmt.Println("Num of fields: ", fieldCount)
    for i := 0; i < fieldCount; i++ {
      // Get individual field details
      field := reflValue.Field(i)
      fmt.Printf("type: %T, value: %v \n", field, field)
    }
  }
}

func main() {
  var myVar interface{} = Person{name: "Snir", age: 27}
  investigateStruct(myVar)
}

Output:

Num of fields:  2
type: reflect.Value, value: Snir 
type: reflect.Value, value: 27 

以上代碼查看了結(jié)構(gòu)體中包含的字段數(shù),以及每個(gè)字段類(lèi)型和值晰奖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谈撒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匾南,更是在濱河造成了極大的恐慌啃匿,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛆楞,死亡現(xiàn)場(chǎng)離奇詭異溯乒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)豹爹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)裆悄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人帅戒,你說(shuō)我怎么就攤上這事灯帮。” “怎么了逻住?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵钟哥,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我瞎访,道長(zhǎng)腻贰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任扒秸,我火速辦了婚禮播演,結(jié)果婚禮上冀瓦,老公的妹妹穿的比我還像新娘。我一直安慰自己写烤,他們只是感情好翼闽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著洲炊,像睡著了一般感局。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暂衡,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天询微,我揣著相機(jī)與錄音,去河邊找鬼狂巢。 笑死撑毛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唧领。 我是一名探鬼主播藻雌,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼斩个!你這毒婦竟也來(lái)了蹦疑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萨驶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后艇肴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體腔呜,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年再悼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了核畴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冲九,死狀恐怖谤草,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莺奸,我是刑警寧澤丑孩,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站灭贷,受9級(jí)特大地震影響温学,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甚疟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一仗岖、第九天 我趴在偏房一處隱蔽的房頂上張望逃延。 院中可真熱鬧,春花似錦轧拄、人聲如沸揽祥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拄丰。三九已至,卻和暖如春是嗜,著一層夾襖步出監(jiān)牢的瞬間愈案,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工鹅搪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留站绪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓丽柿,卻偏偏與公主長(zhǎng)得像恢准,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子甫题,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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