反射
反射是指在程序運行期對程序本身進行訪問和修改的能力。程序在編譯時,變量被轉換為內存地址,變量名不會被編譯器寫入到可執行部分。在運行程序時,程序無法獲取自身的信息。支持反射的語言可以在程序編譯期將變量的反射信息,如字段名稱、類型信息、結構體信息等整合到可執行文件中,並給程序提供接口訪問反射信息,這樣就可以在程序運行期獲取類型的反射信息,並且有能力修改它們。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"` }