從結構體和接口深入理解GO反射

【譯文】原文地址
關于Go反射這個主題,需要理解Go內部關于結構體、接口和類型系統,才能理解反射的底層工作機制。當然,您也使用反射,而不需要深入理解這些細節。本文的目標是向您介紹一些細節,使您能夠更深入地理解反射。但是這些不是嚴格要求的。這篇文章假設您對結構體和接口有基本的了解。你可以通過"Go by example"快速瀏覽下結構體接口,也可以深入學習下Go的結構體接口。

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

維基百科上面解釋到,在計算機科學中,反射是一種能夠對結構體和行為(本人理解為函數或者方法)進行檢查、內省的過程。
反射是程序運行時的操作。它是一種元編程形式,但并不是所有的元編程都是反射。

為什么反射對Go語言重要

反射在很多方面都起作用。通過本文我將關注最明顯的一個作用。Go作為一種靜態編程語言,你必須提前聲明所有類型才可以使用。因此,你沒辦法處理事先不清楚的類型,即使你需要對它進行操作、檢查你也不需要了解這些信息。

一個典型的例子就是fmt包中的print函數。如果你想打印一個變量的類型可以使用%T,fmt包不需要知道你自定義的Person結構體。但是它還是能打印出Person接頭體內容。

空接口

接口是一種定義了多個方法的類型,實現了這些方法的結構體就實現了該接口。這允許將接口作為一種類型傳給方法使用,您可以將實現了該接口的結構體傳入方法。對于一個空接口,每個結構體和每個基本類型,內建實現了空接口。

因此,使用空接口作為類型的函數參數,可以接受任意類型參數。

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

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

上面的代碼可以正常工作,第一個print將打印101,因為對類型int的變量x執行了+1操作,然后將x傳給Println,該函數也是接收一個空接口作為參數,內部是反射實現。

但是Go還是一個靜態類型語言,所以使用空接口將不允許您對變量進行任何其他操作(除非使用類型斷言或反射)。

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

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

上面的代碼無法編譯通過,在myPrint函數中,item是空接口類型,即使底層是整形,但是Go并不知道它,因此代碼會panic。

類型斷言

類型斷言可以幫助你驗證變量的實際類型,如果它是您斷言的類型,就會以這種類型來獲取對應值。

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

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

上面的代碼會打印10,因為我們使用myVar.(int)得到對應的原始類型。斷言成功的話,v將賦值為對應類型變量,ok將賦值為bool值。

關于類型斷言的更多內容,,如果您感興趣的話可以瀏覽類型斷言類型切換。

類型實現細節在哪里呢?

類型斷言(以及代理,反射)是如何知道一個通用接口(空接口)的底層類型的呢。

要理解這點,需要通過/src/sync/atomic/value.go直接看go實現。它實現了go中每個變量的基礎值。

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

空接口和go通過擴展的每個值,在底層表示中包括兩個unsafe指針:typ和data。

  • typ保存當前變量的類型信息,因此即使一個變量是空接口,實際的類型信息在typ中是完整的。
  • data保存值本身,還有其他數據信息如kind值,這個不在本文討論范圍之內。重點是data保存類型的值信息。

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

當你知道要檢查的類型時,斷言允許驗證和使用接口的底層類型值。在前面的例子中,我們專門為int使用斷言。因為我們提前知道其類型,以便斷言可以正常工作。即使我們用switch多個case來檢查,我們仍然必須知道在編譯時斷言的具體類型。

在編譯時不知道具體的類型情況下,就需要反射了。或者換句話說,如本文開頭所述,當我們需要在運行時檢查。回想下fmt例子,fmt包并不知道結構體類型但仍然可以打印它的值和類型(使用%T)。

反射

Reflect.Type和Reflect.Value是反射包提供的兩個基本且最終要的類型。它們是Reflect包中定義的兩個結構體,reflect包有操作接口變量的方法,底層實現其實都是通過將接口的typ和data信息復制到這兩個結構體上。這樣通過這兩個結構體對應的方法即可處理接口了。
Reflect.TypeOf()和Reflect.ValueOf()是兩個可用的基本方法,分別返回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
}

為了更清楚的說明:我們可以看一下自己定義的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返回底層類型,和一個指向值的指針。需要注意的是ValueOf返回的的值類型是Reflect.Value類型的,并不是變量原始類型。

例如,前面的例子中,我們不能取接口的值并對其做算數運算,比如myValue + 1。這是無法通過編譯的,因為Go編譯器無法根據Reflect.Value類型識別這個操作,但對原始類型int是可識別的。

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是有點棘手的,而且網上的一些介紹也會讓您感到困惑,因為大部分介紹kind都是根據type理論,和討論Haskell之類的語言實現。
關于Go你需要知道的是,每個變量都有一個Kind類型,是從type派生出來的。Kind可以理解為是類型中的類型。
最容易說清楚kind概念就是自己定義的結構體。讓我們回到前面創建Person結構體的代碼。我們定義的myPerson是一個Person類型。Person結構體本身類型,即Kind是struct。獲取kind類型可以通過對Reflect.Type變量使用Kind()方法
例如上面的代碼可以修改為:

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

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

對一些例子,不像上面struct比較明顯,Kind看起來似乎有點重復。例如type是int64其Kind也是int64。無需強調的是,我們了解Kind是因為它有助于重用空接口值(譯者:這里似乎沒解釋清楚)。

將Reflect.Value轉換為原始類型值

因此我們根據Reflect.ValueOf()函數可以對一個空接口類型變量進行分析,并得到一個類型為Rreflect.Value值。但是這個值并不正真有用,因為Go類型系統無法識別出它的原始類型。

我們希望將該值轉換成其原始類型。這個過程如下:
1、使用Reflect.TypeOf或Reflect.Kind()識別出其原始類型。
2、使用指向值的指針來獲取原始值(unsafe指針不在本文范圍內)
3、對指針進行類型轉換
很幸運,reflect包已經提供了處理所有的基本類型轉換函數。如下所示:

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
}

所有的基本類型都有轉換方法可用,Bool、Float、String等。

復雜類型的探究

上面介紹了基本類型,但是我們如何處理結構體呢?
reflect包也提供了方法來查看結構體內部信息。如下代碼所示:

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 

以上代碼查看了結構體中包含的字段數,以及每個字段類型和值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容