1. 程式人生 > >Go語言反射(reflect)及應用

Go語言反射(reflect)及應用

# Go語言反射(reflect)及應用 ## 基本原理及應用場景 在編譯時不知道型別的情況下,可更新變數、在執行時檢視值、呼叫方法以及直接對它們的佈局進行操作,這種機制被稱為**反射**。 具體的應用場景大概如下: - 動態地獲取變數的各種資訊(包括變數的型別`type`、類別`kind`); - 如果是結構體變數,還可以獲取結構體本身的欄位、方法; - 可以修改變數的值,呼叫變數的方法; --- 具體應用場景: - 編寫函式的介面卡; ~~~go func funcName(funcPtr interface{},args ...interface{}){} ~~~ 在暫時未知呼叫哪個介面的時候,進行傳參,傳入的是可變引數`args`,這時候配合傳入的函式指標`funcPtr`,利用反射,進行動態地呼叫函式。 ~~~go func testInt(b interface{}) { //獲取型別 rType := reflect.TypeOf(b) fmt.Println("rType:",rType) //獲取值 rVal := reflect.ValueOf(b) n := rVal.Int() fmt.Printf("rVal value:%v , type: %T\n",rVal,rVal) fmt.Printf("n value: %v , type: %T \n",n,n) //獲取interface{} Ir := rVal.Interface() fmt.Printf("Ir , value: %v , type: %T \n",Ir,Ir) //型別斷言 num := Ir.(int) fmt.Printf("num , value: %v , type: %T \n",num,num) } func testStruct(b interface{}) { rType := reflect.TypeOf(b) fmt.Println("rType:",rType) //獲取值 rVal := reflect.ValueOf(b) fmt.Printf("rVal value:%v , type: %T\n",rVal,rVal) //獲取interface{} Ir := rVal.Interface() fmt.Printf("Ir , value: %v , type: %T \n",Ir,Ir) rKind := rVal.Kind() //表示資料類別 fmt.Printf("rkind , kind: %v , type: %T \n",rKind,rKind) //型別斷言 num ,ok:= Ir.(Student) if ok { fmt.Printf("num , value: %v , type: %T \n", num, num) fmt.Println(num.Name) } } ~~~ - 對結構體進行序列化,需要制定`Tag`。 在對函式結構體序列化的時候,自定義`Tag`用到了反射,生成相對應的字串。 --- ## reflect 包及相關常用函式 > ### type [Kind](https://github.com/golang/go/blob/master/src/reflect/type.go?name=release#208) > > ``` > type Kind uint > ``` > > Kind代表Type型別值表示的具體分類。零值表示非法分類。 > ### type [Type](https://github.com/golang/go/blob/master/src/reflect/type.go?name=release#32) > > ``` > type Type interface { > ... > } > ``` > > Type型別用來表示一個go型別。 > > 不是所有go型別的Type值都能使用所有方法。請參見每個方法的文件獲取使用限制。在呼叫有分類限定的方法時,應先使用Kind方法獲知型別的分類。呼叫該分類不支援的方法會導致執行時的panic。 > > > > #### func [TypeOf](https://github.com/golang/go/blob/master/src/reflect/type.go?name=release#1005) > > ``` > func TypeOf(i interface{}) Type > ``` > > TypeOf返回介面中儲存的值的型別,TypeOf(nil)會返回nil。 > ### type [Value](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#61) > > ``` > type Value struct { > // 內含隱藏或非匯出欄位 > } > ``` > > Value為go值提供了反射介面。 > > 不是所有go型別值的Value表示都能使用所有方法。請參見每個方法的文件獲取使用限制。在呼叫有分類限定的方法時,應先使用Kind方法獲知該值的分類。呼叫該分類不支援的方法會導致執行時的panic。 > > Value型別的零值表示不持有某個值。零值的IsValid方法返回false,其Kind方法返回Invalid,而String方法返回"",所有其它方法都會panic。絕大多數函式和方法都永遠不返回Value零值。如果某個函式/方法返回了非法的Value,它的文件必須顯式的說明具體情況。 > > 如果某個go型別值可以安全的用於多執行緒併發操作,它的Value表示也可以安全的用於併發。 > > > > #### func [ValueOf](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#2268) > > ``` > func ValueOf(i interface{}) Value > ``` > > ValueOf返回一個初始化為i介面保管的具體值的Value,ValueOf(nil)返回Value零值。 > > > > #### func (Value) [Kind](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#1154) > > ``` > func (v Value) Kind() Kind > ``` > > Kind返回v持有的值的分類,如果v是Value零值,返回值為Invalid > > > > #### func (Value) [Elem](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#831) > > ``` > func (v Value) Elem() Value > ``` > > Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。如果v的Kind不是Interface或Ptr會panic;如果v持有的值為nil,會返回Value零值。 > > > > #### unc (Value) [NumField](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#1320) > > ``` > func (v Value) NumField() int > ``` > > 返回v持有的結構體型別值的欄位數,如果v的Kind不是Struct會panic > > #### func (Value) [Field](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#866) > > ``` > func (v Value) Field(i int) Value > ``` > > 返回結構體的第i個欄位(的Value封裝)。如果v的Kind不是Struct或i出界會panic > > > > #### func (Value) [NumMethod](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#1289) > > ``` > func (v Value) NumMethod() int > ``` > > 返回v持有值的方法集的方法數目。 > > #### func (Value) [Method](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#1272) > > ``` > func (v Value) Method(i int) Value > ``` > > 返回v持有值型別的第i個方法的已繫結(到v的持有值的)狀態的函式形式的Value封裝。返回值呼叫Call方法時不應包含接收者;返回值持有的函式總是使用v的持有者作為接收者(即第一個引數)。如果i出界,或者v的持有值是介面型別的零值(nil),會panic。 > > #### func (Value) [MethodByName](https://github.com/golang/go/blob/master/src/reflect/value.go?name=release#1304) > > ``` > func (v Value) MethodByName(name string) Value > ``` > > 返回v的名為name的方法的已繫結(到v的持有值的)狀態的函式形式的Value封裝。返回值呼叫Call方法時不應包含接收者;返回值持有的函式總是使用v的持有者作為接收者(即第一個引數)。如果未找到該方法,會返回一個Value零值。 更多其它型別以及函式:[Go語言標準庫文件](https://studygolang.com/static/pkgdoc/pkg/reflect.htm)。 --- ## 注意事項及細節 - 變數、`interface{}` 和 `reflect.Value` 是可以相互轉換的。 ![image-20201103190247769](https://i.loli.net/2020/11/03/WOVYfDB7exyikgu.png) - `reflect.Value.Kind`,獲取變數的類別,返回的是一個常量 - `Type` 和 `Kind` 的區別 `Type` 是型別,` Kind`是類別,` Type 和Kind` 可能是相同的,也可能是不同的。 比如:` var num int` = 10,` num` 的 `Type` 是 `int `, `Kind` 也是 `int`; 比如: `var stu Student stu` 的`Type` 是 `packageXXX.Student` , `Kind `是 `struct `。 - 通過反射的來修改變數, 注意當使用`SetXxx `方法來設定,需要通過傳入對應的指標型別來完成, 這樣才能改變傳入的變數的值; 同時使用到`reflect.Value.Elem()`方法轉換成對應保管的值的`Value`封裝,或者持有的指標指向的值的`Value`封裝。 ~~~go func testElem(b interface{}) { rVal := reflect.ValueOf(b) rVal.Elem().SetInt(20)//Elem()轉換指標為所指向的值,相當於用一個變數引用該指標指向的值 } /* func (Value) Elem eg: func (v Value) Elem() Value Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。 如果v的Kind不是Interface或Ptr會panic;如果v持有的值為nil,會返回Value零值。*/ ~~~ ![image-20201103192118610](https://i.loli.net/2020/11/03/y3eJpnUIShV7Rtq.png) --- ## 例項 > 需求:使用反射來遍歷結構體的欄位,呼叫結構體的方法,修改結構體欄位的值,並獲取結構體標籤的值 ~~~go package main import ( "fmt" "reflect" ) //使用反射來遍歷結構體的欄位,呼叫結構體的方法,修改結構體欄位的值,並獲取結構體標籤的值 //定義結構體 type Student struct { Name string `json:"name"` // 是 ` ` (tab鍵上的~按鍵) ,不是 ' ' Sex string `json:"sex"` Age int `json:"age"` Sal float64 `json:"sal"` } func (s Student) GetName() string { //第0個方法 fmt.Println("該結構體Name欄位值為:",s.Name) return s.Name } func (s *Student) Set(newName string,newAge int,newSal float64){ //第2個方法 s.Name = newName s.Age = newAge s.Sal = newSal s.Print() } func (s Student) Print() { //第1個方法 fmt.Println("呼叫 Print 函式輸出結構體:",s) } //反射獲取結構體欄位、方法,並呼叫 func testReflect(b interface{}) { rVal := reflect.ValueOf(b).Elem() rType := reflect.TypeOf(b).Elem() //判斷是否是結構體在進行下一步操作 if rType.Kind() != reflect.Struct{ fmt.Println("該型別不是結構體。所以無法獲取欄位及其方法。") } //獲取欄位數量 numField := rVal.NumField() fmt.Printf("該結構體有%d個欄位\n",numField) //遍歷欄位 for i := 0; i < numField; i++ { //獲取欄位值、標籤值 rFieldTag := rType.Field(i).Tag.Get("json") if rFieldTag != "" { fmt.Printf("結構體第 %v 個欄位值為:%v ," + "Tag‘json’名為:%v\n",i,rVal.Field(i),rFieldTag) } } //獲取方法數量 numMethod := rVal.NumMethod() //用指標可以獲取到指標接收的方法 fmt.Printf("該結構體有%d個方法\n",numMethod) //呼叫方法(方法順序 按照ACSII碼排序) rVal.Method(0).Call(nil) rVal.Method(1).Call(nil) //引數也需要以 Value 的切片 傳入 params := make([]reflect.Value ,3) params[0] = reflect.ValueOf("hhhh") params[1] = reflect.ValueOf(28) params[2] = reflect.ValueOf(99.9) rVal.Method(2).Call(params) rVal.Method(1).Call(nil) } func main() { stu := Student{ Name: "莉莉安", Sex: "f", Age: 19, Sal: 98.5, } //呼叫編寫的函式並輸出 testReflect(&stu) fmt.Println("主函式輸出結構體 Student :",stu) } ~~~ ![image--20201103](https://i.loli.net/2020/11/03/oP4JXrLQkmjsSeU.png) >
上面方法無法通過呼叫結構體中指標接收的方法,來修改結構體欄位,無法獲取指標接收的修改方法。 已解決,可選思路如下: 1. 可通過直接獲取欄位值進行修改。(不夠便捷) 2. 用指標型別的`reflect.Value`可以獲取到指標接收的方法(同時還包括值接受者的方法),不轉換為指標所指向的值,直接用指標操作即可。 可以識別並使用出指標接收的結構體的所有方法,包括值接收的、指標接收的方法。(前提是原結構體有修改方法) --- `func (Value) Elem()` >Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。 ==注意==:並不是地址,或者指向原值的引用。 --- 結合解決思路,修改結果如下: ~~~go package main import ( "fmt" "reflect" ) //使用反射來遍歷結構體的欄位,呼叫結構體的方法,修改結構體欄位的值,並獲取結構體標籤的值 //定義結構體 type Student struct { Name string `json:"name"` // 是 ` ` (tab鍵上的~按鍵) ,不是 ' ' Sex string `json:"sex"` Age int `json:"age"` Sal float64 `json:"sal"` } func (s Student) GetName() string { //第0個方法 fmt.Println("該結構體Name欄位值為:",s.Name) return s.Name } func (s *Student) Set(newName string,newAge int,newSal float64){ //第2個方法 s.Name = newName s.Age = newAge s.Sal = newSal s.Print() } func (s Student) Print() { //第1個方法 fmt.Println("呼叫 Print 函式輸出結構體:",s) } //反射獲取結構體欄位、方法,並呼叫 func testReflect(b interface{}) { rVal := reflect.ValueOf(b).Elem() rValI := reflect.ValueOf(b) rType := reflect.TypeOf(b).Elem() //判斷是否是結構體在進行下一步操作 if rType.Kind() != reflect.Struct{ fmt.Println("該型別不是結構體。所以無法獲取欄位及其方法。") } //獲取欄位數量 numField := rVal.NumField() fmt.Printf("該結構體有%d個欄位\n",numField) //遍歷欄位 for i := 0; i < numField; i++ { //獲取欄位值、標籤值 rFieldTag := rType.Field(i).Tag.Get("json") if rFieldTag != "" { fmt.Printf("結構體第 %v 個欄位值為:%v ," + "Tag‘json’名為:%v\n",i,rVal.Field(i),rFieldTag) } } //獲取方法數量 numMethod := rValI.NumMethod() //用指標可以獲取到指標接收的方法 fmt.Printf("該結構體有%d個方法\n",numMethod) //呼叫方法(方法順序 按照ACSII碼排序) rVal.Method(0).Call(nil) rVal.Method(1).Call(nil) //引數也需要以 Value 的切片 傳入 params := make([]reflect.Value ,3) params[0] = reflect.ValueOf("hhhh") params[1] = reflect.ValueOf(28) params[2] = reflect.ValueOf(99.9) rValI.Method(2).Call(params) rVal.Method(1).Call(nil) } func main() { stu := Student{ Name: "莉莉安", Sex: "f", Age: 19, Sal: 98.5, } //呼叫編寫的函式並輸出 testReflect(&stu) fmt.Println("主函式輸出結構體 Student :",stu) } ~~~ ![image-20201103175931261](https://i.loli.net/2020/11/03/jzxaIvkHd64pt