Go語言之反射(一)
反射
反射是指在程式執行期對程式本身進行訪問和修改的能力。程式在編譯時,變數被轉換為記憶體地址,變數名不會被編譯器寫入到可執行部分。在執行程式時,程式無法獲取自身的資訊。支援反射的語言可以在程式編譯期將變數的反射資訊,如欄位名稱、型別資訊、結構體資訊等整合到可執行檔案中,並給程式提供介面訪問反射資訊,這樣就可以在程式執行期獲取型別的反射資訊,並且有能力修改它們。Go程式在執行期使用reflect包訪問程式的反射資訊。
C/C++語言沒有支援反射功能,只能通過typeid提供非常弱化的程式執行時型別資訊。Java、C#等語言都支援完整的反射功能。Lua、JavaScript類動態語言,由於其本身的語法特性就可以讓程式碼在執行期訪問程式自身的值和型別資訊,因此不需要反射系統。Go程式的反射系統無法獲取到一個可執行檔案空間中或者是一個包中的所有型別資訊,需要配合使用標準庫中對應的詞法、語法解析器和抽象語法樹(AST)對原始碼進行掃描後獲得這些資訊。
反射的型別物件(reflect.Type)
在Go程式中,使用reflect.TypeOf()函式可以獲得任意值的型別物件(reflect.Type),程式通過型別物件可以訪問任意值的型別資訊。下面通過例子來理解獲取型別物件的過程:
package main import ( "fmt" "reflect" ) func main() { var a int typeOfA := reflect.TypeOf(a) fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
程式碼輸出如下:
int int
程式碼說明如下:
- 第10行,定義一個int型別的變數。
- 第12行,通過reflect.TypeOf()取得變數a的型別物件typeOfA,型別為reflect.Type()。
- 第14行中,通過typeOfA型別物件的成員函式,可以分別獲取到typeOfA變數的型別名為int,種類(Kind)為int。
理解反射的型別(Type)與種類(Kind)
在使用反射時,需要首先理解型別(Type)和種類(Kind)的區別。程式設計中,使用最多的是型別,但在反射中,當需要區分一個大品種的型別時,就會用到種類(Kind)。例如,需要統一判斷型別中的指標時,使用種類(Kind)資訊就較為方便。
1.反射種類(Kind)的定義
Go程式中的型別(Type)指的是系統原生資料型別,如int、string、bool、float32等型別,以及使用type關鍵字定義的型別,這些型別的名稱就是其型別本身的名稱。例如使用typeAstruct{}定義結構體時,A就是struct{}的型別。種類(Kind)指的是物件歸屬的品種,在reflect包中有如下定義:
type Kind uint const ( Invalid Kind = iota // 非法型別 Bool // 布林型 Int // 有符號整型 Int8 // 有符號8位整型 Int16 // 有符號16位整型 Int32 // 有符號32位整型 Int64 // 有符號64位整型 Uint // 無符號整型 Uint8 // 無符號8位整型 Uint16 // 無符號16位整型 Uint32 // 無符號32位整型 Uint64 // 無符號64位整型 Uintptr // 指標 Float32 // 單精度浮點數 Float64 // 雙精度浮點數 Complex64 // 64位複數型別 Complex128 // 128位複數型別 Array // 陣列 Chan // 通道 Func // 函式 Interface // 介面 Map // 對映 Ptr // 指標 Slice // 切片 String // 字串 Struct // 結構體 UnsafePointer // 底層指標 )
Map、Slice、Chan屬於引用型別,使用起來類似於指標,但是在種類常量定義中仍然屬於獨立的種類,不屬於Ptr。type A struct{}定義的結構體屬於Struct種類,*A屬於Ptr。
2.從型別物件中獲取型別名稱和種類的例子
Go語言中的型別名稱對應的反射獲取方法是reflect.Type中的Name()方法,返回表示型別名稱的字串。型別歸屬的種類(Kind)使用的是reflect.Type中的Kind()方法,返回reflect.Kind型別的常量。下面的程式碼中會對常量和結構體進行型別資訊獲取。
package main import ( "fmt" "reflect" ) // 定義一個Enum型別 type Enum int const ( Zero Enum = 0 ) func main() { // 宣告一個空結構體 type cat struct { } // 獲取結構體例項的反射型別物件 typeOfCat := reflect.TypeOf(cat{}) // 顯示反射型別物件的名稱和種類 fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // 獲取Zero常量的反射型別物件 typeOfA := reflect.TypeOf(Zero) // 顯示反射型別物件的名稱和種類 fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
程式碼輸出如下:
cat struct Enum int
程式碼說明如下:
- 第18行,宣告結構體型別cat。
- 第22行,將cat例項化,並且使用reflect.TypeOf()獲取被例項化後的cat的反射型別物件。
- 第25行,輸出cat的型別名稱和種類,型別名稱就是cat,而cat屬於一種結構體種類,因此種類為struct。
- 第28行,Zero是一個Enum型別的常量。這個Enum型別在第9行宣告,第12行聲明瞭常量。如沒有常量也不能建立例項,通過reflect.TypeOf()直接獲取反射型別物件。
- 第31行,輸出Zero對應的型別物件的型別名和種類。
指標與指標指向的元素
Go程式中對指標獲取反射物件時,可以通過reflect.Elem()方法獲取這個指標指向的元素型別。這個獲取過程被稱為取元素,等效於對指標型別變數做了一個*操作,程式碼如下:
package main import ( "fmt" "reflect" ) func main() { // 宣告一個空結構體 type cat struct { } // 建立cat的例項 ins := &cat{} // 獲取結構體例項的反射型別物件 typeOfCat := reflect.TypeOf(ins) // 顯示反射型別物件的名稱和種類 fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind()) // 取型別的元素 typeOfCat = typeOfCat.Elem() // 顯示反射型別物件的名稱和種類 fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind()) }
程式碼輸出如下:
name:'' kind:'ptr' element name: 'cat', element kind: 'struct'
程式碼說明如下:
- 第15行,建立了cat結構體的例項,ins是一個*cat型別的指標變數。
- 第18行,對指標變數獲取反射型別資訊。
- 第21行,輸出指標變數的型別名稱和種類。Go語言的反射中對所有指標變數的種類都是Ptr,但注意,指標變數的型別名稱是空,不是*cat。
- 第24行,取指標型別的元素型別,也就是cat型別。這個操作不可逆,不可以通過一個非指標型別獲取它的指標型別。
- 第27行,輸出指標變數指向元素的型別名稱和種類,得到了cat的型別名稱(cat)和種類(struct)。
使用反射獲取結構體的成員型別
任意值通過reflect.TypeOf()獲得反射物件資訊後,如果它的型別是結構體,可以通過反射值物件(reflect.Type)的NumField()和Field()方法獲得結構體成員的詳細資訊。與成員獲取相關的reflect.Type的方法如下表所示。
方法 | 說明 |
Field(i int) StructField | 根據索引,返回索引對應的結構體欄位的資訊。當值不是結構體或索引超界時發生宕機 |
NumField() int | 返回結構體成員欄位數量。當型別不是結構體或索引超界時發生宕機 |
FieldByName(name string) (StructField, bool) | 根據給定字串返回字串對應的結構體欄位的資訊。沒有找到時bool返回 false,當型別不是結構體或索引超界時發生宕機 |
FieldByIndex(index []int) StructField | 多層成員訪問時,根據[]int 提供的每個結構體的欄位索引,返回欄位的資訊。沒有找到時返回零值。當型別不是結構體或索引超界時發生宕機 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根據匹配函式匹配需要的欄位。當值不是結構體或索引超界時發生宕機 |
1.結構體欄位型別
reflect.Type的Field()方法返回StructField結構,這個結構描述結構體的成員資訊,通過這個資訊可以獲取成員與結構體的關係,如偏移、索引、是否為匿名欄位、結構體標籤(StructTag)等,而且還可以通過StructField的Type欄位進一步獲取結構體成員的型別資訊。StructField的結構如下:
type StructField struct { Name string // 欄位名 PkgPath string // 欄位路徑 Type Type // 欄位反射型別物件 Tag StructTag // 欄位的結構體標籤 Offset uintptr // 欄位在結構體中的相對偏移 Index []int // Type.FieldByIndex中的返回的索引值 Anonymous bool // 是否為匿名欄位 }
欄位說明如下。
- Name:為欄位名稱。
- PkgPath:欄位在結構體中的路徑。
- Type:欄位本身的反射型別物件,型別為reflect.Type,可以進一步獲取欄位的型別資訊。
- Tag:結構體標籤,為結構體欄位標籤的額外資訊,可以單獨提取。
- Index:FieldByIndex中的索引順序。
- Anonymous:表示該欄位是否為匿名欄位。
2.獲取成員反射資訊
下面程式碼中,例項化一個結構體並遍歷其結構體成員,再通過reflect.Type的FieldByName()方法查詢結構體中指定名稱的欄位,直接獲取其型別資訊。
反射訪問結構體成員型別及資訊:
package main import ( "fmt" "reflect" ) func main() { // 宣告一個空結構體 type cat struct { Name string // 帶有結構體tag的欄位 Type int `json:"type" id:"100"` } // 建立cat的例項 ins := cat{Name: "mimi", Type: 1} // 獲取結構體例項的反射型別物件 typeOfCat := reflect.TypeOf(ins) // 遍歷結構體所有成員 for i := 0; i < typeOfCat.NumField(); i++ { // 獲取每個成員的結構體欄位型別 fieldType := typeOfCat.Field(i) // 輸出成員名和tag fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag) } // 通過欄位名, 找到欄位型別資訊 if catType, ok := typeOfCat.FieldByName("Type"); ok { // 從tag中取出需要的tag fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id")) } }
程式碼輸出如下:
name: Name tag: '' name: Type tag: 'json:"type" id:"100"' type 100
程式碼說明如下:
- 第11行,聲明瞭帶有兩個成員的cat結構體。
- 第15行,Type是cat的一個成員,這個成員型別後面帶有一個以`開始和結尾的字串。這個字串在Go語言中被稱為Tag(標籤)。一般用於給欄位新增自定義資訊,方便其他模組根據資訊進行不同功能的處理。
- 第19行,建立cat例項,並對兩個欄位賦值。結構體標籤屬於型別資訊,無須且不能賦值。
- 第22行,獲取例項的反射型別物件。
- 第25行,使用reflect.Type型別的NumField()方法獲得一個結構體型別共有多少個欄位。如果型別不是結構體,將會觸發宕機錯誤。
- 第28行,reflect.Type中的Field()方法和NumField一般都是配對使用,用來實現結構體成員的遍歷操作。
- 第31行,使用reflect.Type的Field()方法返回的結構不再是reflect.Type而是StructField結構體。
- 第35行,使用reflect.Type的FieldByName()根據欄位名查詢結構體欄位資訊,catType表示返回的結構體欄位資訊,型別為StructField,ok表示是否找到結構體欄位的資訊。
- 第38行中,使用StructField中Tag的Get()方法,根據Tag中的名字進行資訊獲取。
結構體標籤(Struct Tag)
通過reflect.Type獲取結構體成員資訊reflect.StructField結構中的Tag被稱為結構體標籤(StructTag)。結構體標籤是對結構體欄位的額外資訊標籤。
JSON、BSON等格式進行序列化及物件關係對映(Object Relational Mapping,簡稱ORM)系統都會用到結構體標籤,這些系統使用標籤設定欄位在處理時應該具備的特殊屬性和可能發生的行為。這些資訊都是靜態的,無須例項化結構體,可以通過反射獲取到。
1.結構體標籤的格式
Tag 在結構體欄位後方書寫的格式如下:
`key1:"value1" key2:"value2"`
結構體標籤由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。鍵值對之間使用一個空格分隔。
2.從結構體標籤中獲取值
StructTag擁有一些方法,可以進行Tag資訊的解析和提取,如下所示:
- func (tag StructTag) Get(key string) string:根據Tag中的鍵獲取對應的值,例如`key1:"value1"key2:"value2"`的Tag中,可以傳入“key1”獲得“value1”。
- func (tag StructTag) Lookup(key string) (value string, ok bool):根據Tag中的鍵,查詢值是否存在。
3.結構體標籤格式錯誤導致的問題
編寫Tag時,必須嚴格遵守鍵值對的規則。結構體標籤的解析程式碼的容錯能力很差,一旦格式寫錯,編譯和執行時都不會提示任何錯誤,參見下面這個例子:
package main import ( "fmt" "reflect" ) func main() { type cat struct { Name string Type int `json: "type" id:"100"` } typeOfCat := reflect.TypeOf(cat{}) if catType, ok := typeOfCat.FieldByName("Type"); ok { fmt.Println(catType.Tag.Get("json")) } }
程式碼輸出空字串,並不會輸出期望的type。第12行中,在json:和"type"之間增加了一個空格。這種寫法沒有遵守結構體標籤的規則,因此無法通過Tag.Get獲取到正確的json對應的值。這個錯誤在開發中非常容易被疏忽,造成難以察覺的錯誤。所以,修改上述程式碼第12行為如下程式碼,則可以正常列印。
type cat struct { Name string Type int `json:"type" id:"100"` }