go語言reflect包最佳實踐之struct操作(遍歷、賦值與方法調用)


go語言reflect包最佳實踐之struct操作(遍歷、賦值與方法調用)

1. 反射基本概念

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

golang中的接口值是兩字節的數據結構,兩個字節各是一個指針,其中:

  • 第一個指針指向一個叫做iTable的內部表,表中包含兩方面內容,一是值的類型信息,二是值的方法集
  • 第二個指針指向實際存儲的值。

對應地,任意接口值在go反射中都分為reflect.Type和reflect.Value兩部分,我們可分別通過reflect.TypeOf()和reflect.ValueOf()函數對象的Type和Value。

本文將使用reflect包對結構體進行遍歷、賦值與方法調用操作,以熟悉了解reflect包的基本概念與使用。

2. struct字段的遍歷

2.1 簡單結構體的遍歷

首先定義一個如下的簡單結構體,如程序所示,共有三個字段。

type Employee struct {
	Name   string
	Role   string
	Salary float64
}

下面,我們嘗試用reflect包對一個Employee類型的值進行遍歷,要求輸出字段的名稱、類型和值。

var xiaowang = &Employee{
	Name:   "xiaowang",
	Role:   "glory engineer",
	Salary: 0.5,
}

func traverse(target interface{}) {
	sVal := reflect.ValueOf(target)
	sType := reflect.TypeOf(target)
	if sType.Kind() == reflect.Ptr {
        //用Elem()獲得實際的value
		sVal = sVal.Elem()
		sType = sType.Elem()
	}
	num := sVal.NumField()
	for i := 0; i < num; i++ {
		f := sType.Field(i)
		val := sVal.Field(i).Interface()
		fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
	}
}

func main() {
	traverse(xiaowang)
}

需要注意的是,程序在正式遍歷字段前,對種類(Kind)為指針(reflect.Ptr)的值調用了Elem()方法,令其指向實際的值。

(而事實上,reflect.Value.NumField()與reflect.Value.Field()等方法均需要調用者的種類(Kind)為結構體(reflect.Struct),否則程序會panic。)

運行程序,輸出如下,可見已成功遍歷了結構體的各字段

Name string = xiaowang
Role string = glory engineer
Salary float64 = 0.5

2.2 復雜結構體的遍歷

考慮字段類型為結構體的特殊情況,比如

type ComplexStruct struct {
	CField1 string
	CField2 *SimpleStruct
	CField3 []string
}

我們需要將程序簡單修改為遞歸遍歷的結構,如下所示

var ComplexTarget = &ComplexStruct{
	CField1: "CValue1",
	CField2: SimpleTarget,
	CField3: []string{"CValue3", "sadf"},
}

func traverse2(target interface{}) {
	sVal := reflect.ValueOf(target)
	sType := reflect.TypeOf(target)
	if sType.Kind() == reflect.Ptr {
		sVal = sVal.Elem()
		sType = sType.Elem()
	}
	num := sVal.NumField()
	for i := 0; i < num; i++ {
        //判斷字段是否為結構體類型,或者是否為指向結構體的指針類型
		if sVal.Field(i).Kind() == reflect.Struct || (sVal.Field(i).Kind() == reflect.Ptr && sVal.Field(i).Elem().Kind() == reflect.Struct) {
			traverse2(sVal.Field(i).Interface())
		} else {
			f := sType.Field(i)
			val := sVal.Field(i).Interface()
			fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
		}
	}
}

func main() {
	traverse2(ComplexTarget)
}

每次訪問字段都先判斷其是否為結構體類型,或者是否為指向結構體的指針類型,若是則遞歸調用遞歸方法,否則直接輸出訪問結果即可。

運行程序,可得如下輸出,可見已實現了期望的遞歸遍歷的功能。

TeamName string = urTeam
Name string = xiaowang
Role string = glory engineer
Salary float64 = 0.5
Duty []string = [code debug]

3. struct賦值操作(根據map構建新struct)

給定一個map值,我們根據該map提供的信息,恢復構建出一個Employee類型的值

var employeeData = map[string]interface{}{
	"name":   "laozhang",
	"role":   "annother glory engineer",
	"salary": 1.5,
}

針對employeeData中key與結構體中字段名大小寫不一致的問題,我們在Employee結構體定義中,給字段加入一些tag信息。(由於map信息往往由外部給出,其key不一定滿足go的字段命名習慣,故直接修改字段名的方法來達到兩者一致是不合適的。)

type Employee struct {
	Name   string  `key:"name"`
	Role   string  `key:"role"`
	Salary float64 `key:"salary"`
}

在對結構體對象賦值過程中,需要注意兩方面的內容:

  1. 用reflect.Value.Set()方法給對應字段賦值,注意該方法的傳入參數是reflect.Value類型的
  2. 在給字段賦值前需要進行類型檢查,若map中的value和字段類型一致,則可以直接調用Set()方法賦值;若兩者類型不一致,則需調用reflect.Type.ConvertibleTo()方法來判斷是否可以進行類型轉換,若可轉換則調用reflect.Value.Convert()方法轉換傳參類型,倘若不可轉換而強行調用Convert()方法,會導致程序panic。為了簡便起見,若類型不可轉換,我們在程序中同樣返回panic並給出錯誤信息。
func rebuiltStruct(mapData map[string]interface{}, target interface{}) {
	sVal := reflect.ValueOf(target)
	sType := reflect.TypeOf(target)
	if sType.Kind() == reflect.Ptr {
		sVal = sVal.Elem()
		sType = sType.Elem()
	}
	num := sVal.NumField()
	for i := 0; i < num; i++ {
		f := sType.Field(i)
		val := sVal.Field(i)
		key := f.Tag.Get("key")
		if dataVal, ok := mapData[key]; ok {
			//類型判斷與轉換
			dataType := reflect.TypeOf(dataVal)
			fieldType := val.Type()
			if dataType == fieldType {
				val.Set(reflect.ValueOf(dataVal))
			} else {
				if dataType.ConvertibleTo(fieldType) {
					val.Set(reflect.ValueOf(dataVal).Convert(fieldType))
				} else {
                    panic(fmt.Sprintf("failed to convert from %s to %s \n", dataType, fieldType))
				}
			}
		} else {
			fmt.Printf("key %s not found in struct definition! \n", key)
		}
	}
	traverse2(target)
}
func main() {
	rebuiltStruct(employeeData, &Employee{})
}

運行上述程序,可得輸出如下

Name string = laozhang
Role string = annother glory engineer
Salary float64 = 1.5

4. 調用struct的方法

4.1 方法遍歷與無參調用

首先,我們對Employee結構體增加兩個方法:

func (e Employee) Code() {
	fmt.Printf("I like to code \n")
}

func (e Employee) Debug() {
	fmt.Printf("I dislike to debug \n")
}

func (e Employee) raiseSalary() {
    fmt.Printf("I want to raise my salasy \n")
}

接着我們編寫程序,用reflect遍歷調用兩個方法:

func callMethodWithReflect(x interface{}) {
	t := reflect.TypeOf(x).Elem()
	v := reflect.ValueOf(x).Elem()
	if t.Kind() == reflect.Ptr {
		v = v.Elem()
		t = t.Elem()
	}
	//NumMethod()只會計算導出的方法,即首字母大寫的方法
	numOfMethod := v.NumMethod()
	fmt.Printf("We have %v methods. \n", numOfMethod)

	//以索引的方式遍歷調用所有的方法
	for i := 0; i < numOfMethod; i++ {
		methodName := t.Method(i).Name
		methodType := t.Method(i).Type
		//方法的Type也可以用v.Method(i).Type()獲得
		fmt.Printf("method-%v name is %s \n", i, methodName)
		fmt.Printf("method-%v type is %s \n", i, methodType)
		args := []reflect.Value{} //不含參調用
		v.Method(i).Call(args)
	}
}

運行程序可得如下結果,發現程序只調用了Code()和Debug(),忽略了raiseSalary()方法。這是因為reflect.Value.NumMethod()只能發現導出(首字母大寫)的方法,而reflect.Type.Method()也只能訪問到導出的方法。

需要注意的是,reflect.Type.Method()方法返回的仍是一個reflect.Value類型的值,只不過其Kind為Func。在調用方法時,reflect.Value.Call()方法的入參是一個reflect.Value的切片,且若Call()方法的調用者的Kind不是Func種類,程序會panic。

We have 2 methods.
method-0 name is Code 
method-0 type is func(main.Employee) 
I like to code 
method-1 name is Debug 
method-1 type is func(main.Employee) 
I dislike to debug 

因此若修改方法raiseSalary()為導出方法,

func (e Employee) RaiseSalary() {
    fmt.Printf("I want to raise my salary \n")
}

再重新執行程序可以發現三個方法均已被遍歷到

We have 3 methods. 
...
...
method-2 name is RaiseSalary 
method-2 type is func(main.Employee) 
I want to raise my salary 

4.2 按方法名調用

我們再增加一個含參數方法

func (e Employee) Work(i int) {
	fmt.Printf("I work for %v hours per day. \n", i)
}

下面,通過指定方法名的方法調用它

func callByMethodName(x interface{}) {
	t := reflect.TypeOf(x).Elem()
	v := reflect.ValueOf(x).Elem()
	if t.Kind() == reflect.Ptr {
		v = v.Elem()
		t = t.Elem()
	}

	//通過方法名調用指定方法,這里同樣無法調用未導出的方法
    printMethod := v.MethodByName("Work")
    
	//此處需注意判斷Zero Value
	if printMethod.IsValid() {
		args := []reflect.Value{reflect.ValueOf(10)}
		printMethod.Call(args)
	} else {
		fmt.Printf("method not found!")
	}
}

這里需要注意以下容易踩坑的地方:

  1. reflect.Value.MethodByName()同樣只能訪問到導出了的方法,若傳入了未導出的方法名,方法會返回一個零值(Zero Value),而不會報錯
  2. 若直接對零值(Zero Value)調用Call()方法,程序會panic,故在調用Call()之前需驗證返回值是否為零值。
  3. 驗證一個值是否是零值(Zero Value),千萬不可以用isZero()方法,否則程序會panic。這是因為零值不是IsZero方法返回true的值,而是IsValid方法返回false的值。在golang文檔中,有如下說明:"The zero Value represents no value. Its IsValid method returns false, its Kind method returns Invalid, its String method returns "", and all other methods panic." 即,零值的IsValid方法返回false,其Kind方法返回Invalid,其String方法返回"",對零值的任何其他方法的調用均會導致panic

參考鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM