介面和反射的關係
介面是 Go 中用於抽象的基本工具之一。介面在值進行分配的時候儲存型別資訊。反射則是在執行時檢查型別和值的方法。
Go 通過 reflect
包實現了反射。該包提供了一些型別和方法用於檢查介面結構部分,不僅如此,它還可以在執行時進行值的修改。
在這篇文章中,我希望能說明介面結構的各部分和反射 API 之間的關係,並最終使得反射包變得更加容易理解。
向一個介面分配一個值
一個介面編碼了三件事:值,方法集,以及所儲存的值的型別。
下圖展示了一個介面的內部結構。
我們可以在該圖中很清楚地看到介面內部結構中的三個部分: _type
表示型別資訊, *data
是一個指向實際值的指標, itab
則編碼了方法集。
當一個方法接收一個介面作為引數時,將一個值傳遞給該函式則會將該值,該值的方法集,和型別打包到介面中。
通過反射包在執行時檢查介面資料
一旦一個值儲存進介面,你就可以使用 reflect
包來檢查該介面的各個部分。我們不能直接檢查該介面的結構;而是通過反射包維護著我們自己有權訪問的介面結構的副本。
即使我們通過介面物件訪問介面,但和直接訪問相關的底層的介面物件有相同效果。
型別 reflect.Type
和 reflect.Value
提供了可以訪問介面結構部分的方法。
reflect.Type
側重於公開型別相關的資料,因此它只限於結構的 _type
部分,而 reflect.Value
則必須將型別資訊與值結合起來,以允許程式員檢查和操作值,因此必須要檢視 _type
以及 *data
部分。
reflect.Type
- 檢查型別
reflect.TypeOf()
函式用於從一個值中提取該值的型別資訊。因為該函式唯一的引數是一個空介面 interface{}
,傳遞給它的值會被分配到該空介面上,因此該值的型別,方法集合值我們都可以輕鬆地獲得。
reflect.TypeOf()
返回一個 reflect.Type
型別的值,它提供了一些方法以允許你可以檢查傳入的值的型別資訊。
下面是一些可用的 Type
方法以及它們返回的與介面相對應的位。
relfect.Type
使用示例
package main import ( "log" "reflect" ) type Gift struct { Senderstring Recipient string Numberuint Contentsstring } func main() { g := Gift{ Sender:"Hank", Recipient: "Sue", Number:1, Contents:"Scarf", } t := reflect.TypeOf(g) if kind := t.Kind(); kind != reflect.Struct { log.Fatalf("This program expects to work on a struct; we Got a %v instead.", kind) } for i := 0; i < t.NumField(); i++ { f := t.Field(i) log.Printf("Field %03d: %-10.10s %v", i, f.Name, f.Type.Kind()) } }
此程式的目的是打印出 Gift
結構體的所有欄位。當我們把 g
傳遞給 reflect.TypeOf()
時,實際上 g
被分配給了被編譯器使用相應的型別和方法集填充的介面。這就允許我們可以遍歷該介面型別部分中的 []fileds
,然後我們得到如下輸出:
2018/12/16 12:00:00 Field 000: Senderstring 2018/12/16 12:00:00 Field 001: Recipientstring 2018/12/16 12:00:00 Field 002: Numberuint 2018/12/16 12:00:00 Field 003: Contentsstring
reflect.Method
- 檢查 itab
和方法集
reflect.Type
型別也會允許你訪問 itab
部分,來從介面中提取出方法資訊。
通過反射檢查方法
package main import ( "log" "reflect" ) type Reindeer string func (r Reindeer) TakeOff() { log.Printf("%q lifts off.", r) } func (r Reindeer) Land() { log.Printf("%q gently lands.", r) } func (r Reindeer) ToggleNose() { if r != "rudolph" { panic("invalid reindeer operation") } log.Printf("%q nose changes state.", r) } func main() { r := Reindeer("rudolph") t := reflect.TypeOf(r) for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) log.Printf("%s", m.Name) } }
這段程式碼完全迭代了儲存在 itab
的函式資料,並顯示了每個方法的名稱:
2018/12/16 12:00:00 Land 2018/12/16 12:00:00 TakeOff 2018/12/16 12:00:00 ToggleNose
reflect.Value
- 檢查值
目前為止我們僅僅討論了型別資訊 - 欄位,方法等。 reflect.Value
則向我們展示了儲存在介面中的實際值的資訊。
與 reflect.Value
相關的方法必然會將型別資訊和實際的值組合在一起。例如,為了從一個結構中提取欄位資訊, refelct
包必須將結構的佈局知識 - 特別是關於儲存在 _type
中的欄位和欄位偏移量的資訊 - 與介面的 *data
部分所指向的實際值結合起來,以便正確地解碼結構。
觀察修改值的示例
package main import ( "log" "reflect" ) type Child struct { Namestring Grade int Nicebool } type Adult struct { Namestring Occupation string Nicebool } // search a slice of structs for Name field that is "Hank" and set its Nice // field to true. func nice(i interface{}) { // retrieve the underlying value of i.we know that i is an // interface. v := reflect.ValueOf(i) // we're only interested in slices to let's check what kind of value v is. if // it isn't a slice, return immediately. if v.Kind() != reflect.Slice { return } // v is a slice.now let's ensure that it is a slice of structs.if not, // return immediately. if e := v.Type().Elem(); e.Kind() != reflect.Struct { return } // determine if our struct has a Name field of type string and a Nice field // of type bool st := v.Type().Elem() if nameField, found := st.FieldByName("Name"); found == false || nameField.Type.Kind() != reflect.String { return } if niceField, found := st.FieldByName("Nice"); found == false || niceField.Type.Kind() != reflect.Bool { return } // Set any Nice fields to true where the Name is "Hank" for i := 0; i < v.Len(); i++ { e := v.Index(i) name := e.FieldByName("Name") nice := e.FieldByName("Nice") if name.String() == "Hank" { nice.SetBool(true) } } } func main() { children := []Child{ {Name: "Sue", Grade: 1, Nice: true}, {Name: "Ava", Grade: 3, Nice: true}, {Name: "Hank", Grade: 6, Nice: false}, {Name: "Nancy", Grade: 5, Nice: true}, } adults := []Adult{ {Name: "Bob", Occupation: "Carpenter", Nice: true}, {Name: "Steve", Occupation: "Clerk", Nice: true}, {Name: "Nikki", Occupation: "Rad Tech", Nice: false}, {Name: "Hank", Occupation: "Go Programmer", Nice: false}, } log.Printf("adults before nice: %v", adults) nice(adults) log.Printf("adults after nice: %v", adults) log.Printf("children before nice: %v", children) nice(children) log.Printf("children after nice: %v", children) }
2018/12/16 12:00:00 adults before nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer false}] 2018/12/16 12:00:00 adults after nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer true}] 2018/12/16 12:00:00 children before nice: [{Sue 1 true} {Ava 3 true} {Hank 6 false} {Nancy 5 true}] 2018/12/16 12:00:00 children after nice: [{Sue 1 true} {Ava 3 true} {Hank 6 true} {Nancy 5 true}]
在最後一個例子中,我們將我們學到的東西結合起來,通過 reflect.Value
來修改一個值。在這個用例中,有人編寫了一個名為 nice()
(可能是 Hank) 的函式,該函式將切片中任何一個結構中名為 Hank
的 nice
欄位都設定為 true
。
需要注意的是, nice()
能夠修改所有傳遞給它的切片,而且它實際的接收型別並不重要 - 只要它是一個元素為結構體的切片,並且具有 Name
和 Nice
欄位即可。
結論
Go 中的反射是使用介面以及 reflect
包實現的。它並沒有什麼神奇之處 - 當你在使用反射時,你可以訪問介面的各個部分以及儲存在其中的值。
通過這種方式,介面的行為就像一面鏡子,允許程式檢查自己。
雖然 Go 是一門靜態型別語言,但通過反射和介面的結合,使得它擁有強大的能力,這通常只存在於動態型別語言當中。
有關 Go 中反射的更多資訊,請務必閱讀 reflect
包文件以及其他和反射相關的精彩部落格文章。