Go語言反射(reflect)及應用

凌丹妙耀發表於2020-11-03

Go語言反射(reflect)及應用

基本原理及應用場景

在編譯時不知道型別的情況下,可更新變數、在執行時檢視值、呼叫方法以及直接對它們的佈局進行操作,這種機制被稱為反射

具體的應用場景大概如下:

  • 動態地獲取變數的各種資訊(包括變數的型別type、類別kind);
  • 如果是結構體變數,還可以獲取結構體本身的欄位、方法;
  • 可以修改變數的值,呼叫變數的方法;

具體應用場景:

  • 編寫函式的介面卡;

    func funcName(funcPtr interface{},args ...interface{}){}
    

    在暫時未知呼叫哪個介面的時候,進行傳參,傳入的是可變引數args,這時候配合傳入的函式指標funcPtr,利用反射,進行動態地呼叫函式。

    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

type Kind uint

Kind代表Type型別值表示的具體分類。零值表示非法分類。

type Type

type Type interface {
...
}

Type型別用來表示一個go型別。

不是所有go型別的Type值都能使用所有方法。請參見每個方法的文件獲取使用限制。在呼叫有分類限定的方法時,應先使用Kind方法獲知型別的分類。呼叫該分類不支援的方法會導致執行時的panic。

func TypeOf

func TypeOf(i interface{}) Type

TypeOf返回介面中儲存的值的型別,TypeOf(nil)會返回nil。

type Value

type Value struct {
    // 內含隱藏或非匯出欄位
}

Value為go值提供了反射介面。

不是所有go型別值的Value表示都能使用所有方法。請參見每個方法的文件獲取使用限制。在呼叫有分類限定的方法時,應先使用Kind方法獲知該值的分類。呼叫該分類不支援的方法會導致執行時的panic。

Value型別的零值表示不持有某個值。零值的IsValid方法返回false,其Kind方法返回Invalid,而String方法返回"",所有其它方法都會panic。絕大多數函式和方法都永遠不返回Value零值。如果某個函式/方法返回了非法的Value,它的文件必須顯式的說明具體情況。

如果某個go型別值可以安全的用於多執行緒併發操作,它的Value表示也可以安全的用於併發。

func ValueOf

func ValueOf(i interface{}) Value

ValueOf返回一個初始化為i介面保管的具體值的Value,ValueOf(nil)返回Value零值。

func (Value) Kind

func (v Value) Kind() Kind

Kind返回v持有的值的分類,如果v是Value零值,返回值為Invalid

func (Value) Elem

func (v Value) Elem() Value

Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。如果v的Kind不是Interface或Ptr會panic;如果v持有的值為nil,會返回Value零值。

unc (Value) NumField

func (v Value) NumField() int

返回v持有的結構體型別值的欄位數,如果v的Kind不是Struct會panic

func (Value) Field

func (v Value) Field(i int) Value

返回結構體的第i個欄位(的Value封裝)。如果v的Kind不是Struct或i出界會panic

func (Value) NumMethod

func (v Value) NumMethod() int

返回v持有值的方法集的方法數目。

func (Value) Method

func (v Value) Method(i int) Value

返回v持有值型別的第i個方法的已繫結(到v的持有值的)狀態的函式形式的Value封裝。返回值呼叫Call方法時不應包含接收者;返回值持有的函式總是使用v的持有者作為接收者(即第一個引數)。如果i出界,或者v的持有值是介面型別的零值(nil),會panic。

func (Value) MethodByName

func (v Value) MethodByName(name string) Value

返回v的名為name的方法的已繫結(到v的持有值的)狀態的函式形式的Value封裝。返回值呼叫Call方法時不應包含接收者;返回值持有的函式總是使用v的持有者作為接收者(即第一個引數)。如果未找到該方法,會返回一個Value零值。

更多其它型別以及函式:Go語言標準庫文件


注意事項及細節

  • 變數、interface{}reflect.Value 是可以相互轉換的。

    image-20201103190247769

  • reflect.Value.Kind,獲取變數的類別,返回的是一個常量

  • TypeKind 的區別

    Type 是型別, Kind是類別, Type 和Kind 可能是相同的,也可能是不同的。

    比如: var num int = 10, numTypeint , Kind 也是 int

    比如: var stu Student stuTypepackageXXX.Student , Kind struct

  • 通過反射的來修改變數, 注意當使用SetXxx 方法來設定,需要通過傳入對應的指標型別來完成, 這樣才能改變傳入的變數的值;

    同時使用到reflect.Value.Elem()方法轉換成對應保管的值的Value封裝,或者持有的指標指向的值的Value封裝。

    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


例項

需求:使用反射來遍歷結構體的欄位,呼叫結構體的方法,修改結構體欄位的值,並獲取結構體標籤的值

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

上面方法無法通過呼叫結構體中指標接收的方法,來修改結構體欄位,無法獲取指標接收的修改方法。

已解決,可選思路如下:

  1. 可通過直接獲取欄位值進行修改。(不夠便捷)

  2. 用指標型別的reflect.Value可以獲取到指標接收的方法(同時還包括值接受者的方法),不轉換為指標所指向的值,直接用指標操作即可。

    可以識別並使用出指標接收的結構體的所有方法,包括值接收的、指標接收的方法。(前提是原結構體有修改方法)


func (Value) Elem()

Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。

注意:並不是地址,或者指向原值的引用。


結合解決思路,修改結果如下:

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

相關文章