Go語言反射(reflect)及應用
阿新 • • 發佈:2020-11-03
# 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