【譯文】原文地址
關于Go反射這個主題,需要理解Go內部關于結構體、接口和類型系統,才能理解反射的底層工作機制。當然,您也使用反射,而不需要深入理解這些細節。本文的目標是向您介紹一些細節,使您能夠更深入地理解反射。但是這些不是嚴格要求的。這篇文章假設您對結構體和接口有基本的了解。你可以通過"Go by example"快速瀏覽下結構體和接口,也可以深入學習下Go的結構體和接口。
什么是反射
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
以上代碼查看了結構體中包含的字段數,以及每個字段類型和值。