Golang反射入門

什麼是反射?

反射的概念是由Smith在1982年首次提出的,主要是指程序可以訪問、檢測和修改它本身狀態或行爲的一種能力。應用能夠通過採用某種機制來實現對自己行爲的描述(self-representation)和監測(examination),並能根據自身行爲的狀態和結果,調整或修改應用所描述行爲的狀態和相關的語義。

其含義就是我們可以通過語言提供的反射功能,在程序運行過程中動態的調用方法、獲取並改變變量屬性,其核心就是其動態性。一般情況下,代碼邏輯在編譯時就已經確定了,變量的類型,方法調用關係都是確定的,但是有了反射,我們可以根據外部輸入,動態的調整變量類型與屬性、調用不同的方法。

Go語言反射

基本概念

Go 語言中通過 reflect包來實現對反射的支持,reflect包中最重要的兩個方法就是 reflect.Typeofreflect.ValueOf

  • reflect.Typeof: 獲取變量的類型信息,其返回的是名爲Type 的接口,接口中實現了衆多的方法,用來獲取任意變量的類型,關於Type 接口的方法詳情,可以參考:Type接口函數全解析

  • reflect.ValueOf:獲取變量的類型信息,其返回的是名爲Value的結構體,用來獲取任意變量的值

從以上,我們還可以獲取到的信息就是,Go語言中,對於一個變量,我們需要從兩個維度來描述它,分別是 類型

反射對象的獲取

func main() {
	a := 100
	aType := reflect.TypeOf(a) // 獲取變量類型
	aValue := reflect.ValueOf(a) // 獲取變量值
	fmt.Println(aType) // int
	fmt.Println(aValue) // 100
}

通過以上代碼,我們就實現了對反射對象的獲取,下面我們看下 reflect.TypeOfreflect.ValueOf的定義

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value 

從這兩個函數的實現,我們可以看出其參數值都是interface,但是我們一般很少直接傳入interface類型,都是像stringint等的具體類型,這裏就涉及到變量類型的隱式轉換的過程:

具體類型 -> interface類型 -> 反射類型

由於任何具體類型,都是可以賦值給反射類型的,所以從 具體類型 -> interface類型的過程中被隱藏,如下方式我們顯式的來進行類型轉換:

func main() {
	a := 100
	var b interface{} = a // 此過程可以通過隱式轉換而忽略

	bType := reflect.TypeOf(b) // 獲取變量類型
	bValue := reflect.ValueOf(b) // 獲取變量值
	fmt.Println(bType) // int
	fmt.Println(bValue) // 100
}

反射類型向基礎類型的轉換

上面提到了具體類型反射類型的轉換,那麼,是否可以將反射類型基礎類型進行轉換呢?答案是肯定的,我們可以使用reflect.Value.Interface 方法來實現從反射類型基礎類型的轉換,我們先看看reflect.Value.Interface方法的定義:

func (v Value) Interface() (i interface{}) 

它返回的是 interface類型的對象,那既然上面都可以將基礎類型直接轉換成interface類型,那麼我們能不能也直接通過如下方法,將interface類型直接賦值給基礎類型,從而直接獲取到基礎類型呢?

func main() {
	a := 100
	var b int
	aValue := reflect.ValueOf(a) // 獲取變量值
	b = aValue.Interface() // error: cannot use aValue.Interface() (type interface {}) as type int in assignment: need type assertion
	fmt.Println(b)
}

是不行的,編譯器報出了cannot use aValue.Interface() (type interface {}) as type int in assignment: need type assertion的錯誤,其意思就是不能將 interface {} 類型轉換成int類型,需要類型斷言,這是由於 interface型-> 基礎類型的轉換必須是顯式的,需要通過類型斷言實現:

func main() {
	a := 100
	var b int
	aValue := reflect.ValueOf(a) // 獲取變量值
	b = aValue.Interface().(int) // 類型斷言
	fmt.Println(b) // 100
}

修改反射對象值

在學習這一部分之前,我們先了解下與之相關的重要方法:

  • Elem:獲取反射對象指向的元素值,類似於Go語言中的* 操作,取值的對象值必須是接口或指針,否則會panic
  • Addr:對於可尋址的值,返回值的地址
  • CanAddr:判斷值是否可尋址
  • CanSet:判斷值是否可被設置
func main() {
	a := 100
	aValue := reflect.ValueOf(a)
	fmt.Println(aValue.CanSet()) // false
	fmt.Println(aValue.CanAddr()) // false
	fmt.Println(aValue.Elem()) // panic: reflect: call of reflect.Value.Elem on int Value
}

由於Go語言中,方法的傳參都是值傳遞的,所以 aValue只是a的副本的反射對象,因此是不可被設置值,也是不可被取地址的,aValue.Elem()報錯原因是a不是指針或接口類型,而是int類型

func main() {
	a := 100
	aValue := reflect.ValueOf(a)
    bValue := reflect.ValueOf(&a)
	fmt.Println(bValue.CanSet()) // false
	fmt.Println(bValue.CanAddr()) // false
	cValue := bValue.Elem()
	fmt.Println(cValue.CanSet()) // true
	fmt.Println(cValue.CanAddr()) // true
	fmt.Println(cValue.Addr()) // 0xc000096000
	fmt.Println(bValue) // 0xc000096000
	fmt.Println(&a) // 0xc000096000
	cValue.SetInt(200) // 修改反射對象值
	fmt.Println(cValue) // 200
	fmt.Println(aValue) // 100
	fmt.Println(a) // 200
}

bValue 只是 &a(a的地址) 的拷貝,因此也是不可被設置值、不可被取地址的,但是通過 bValue.Elem(),相當於是獲取了地址所指向的值,而這個值就是a,因此是可以取地址的,從相同的地址也可以看出來。通過cValue.SetInt(200)將a的值設置爲200,但是aValue的值仍爲100,因此印證了其只是a的拷貝。

對於可以設置值的反射對象,可以用reflect.Value提供的SetSetXxx方法來設置新的值。

反射應用

如下代碼實現了通過命令行調用對應方法的代碼:

type MyFuncLib struct{}

func (m MyFuncLib) HelloWorld() {
	fmt.Println("Hello World!")
}

func (m MyFuncLib) Hello(name string) {
	fmt.Println("hello,", name)
}

func (m MyFuncLib) Hellos(names ...string) {
	for _, name := range names {
		fmt.Println("hello,", name)
	}
}

func (m MyFuncLib) Say(prefix string, names ...string) {
	for _, name := range names {
		fmt.Println(prefix, ",", name)
	}
}

func main() {
	// 參數校驗,參數數量小於2 一定不合法
	if len(os.Args) < 2 {
		panic("參數太少!")
	}

	mf := MyFuncLib{}
	mfType := reflect.TypeOf(mf)
	mfValue := reflect.ValueOf(mf)

	// MethodByName: 通過名稱來獲取方法信息,如果 ok 爲 false, 表示無法獲取到方法
	if method, ok := mfType.MethodByName(os.Args[1]); ok {
		// IsVariadic:判斷方法是否存在可變參數
		if method.Type.IsVariadic() {
			// 方法存在一個默認的參數 mfValue,且可變參可以傳入0或多個參數
			// 故NumIn比實際必須要手動傳入的參數多2
			// 參數數量需滿足 NumIn() - 2 <= os.Args - 2
			if method.Type.NumIn() > len(os.Args) {
				panic("參數太少!")
			}
		} else {
			// 方法存在一個默認的參數 mfValue,故NumIn比實際需要手動傳入的參數多1
			// NumIn() - 1 == os.Args - 2
			if method.Type.NumIn() != len(os.Args)-1 {
				panic("參數數量不匹配!")
			}
		}

		// 方法存在一個默認的參數 mfValue
		args := []reflect.Value{mfValue}

		// 如果存在額外的參數
		if len(os.Args) > 2 {
			for i, arg := range os.Args[2:] {
				argV := reflect.ValueOf(arg)
				argT := reflect.TypeOf(arg)

				var index int

				// 判斷 當前方式是否存在變參 & 當前參數是否是變參的參數
				// 變參 一定是方法最後一個參數,如果參數數量大於等於參數總數,則一定是變參參數
				if method.Type.IsVariadic() && i+1 >= method.Type.NumIn()-1 {
					// 變參是方法最後一個參數
					index = method.Type.NumIn() - 1

					// 判斷變參的值是否合法
					// Elem:對於array,Elem方法可以獲取數組存儲的類型,如 對於[]string 返回 string
					// ConvertibleTo:判斷參數 argT 類型是否可以轉換成參數類型 method.Type.In(index).Elem()
					if !argT.ConvertibleTo(method.Type.In(index).Elem()) {
						panic(fmt.Sprintf("%s can't ConvertibleTo %s", argT, method.Type.In(index).Elem()))
					}
					// Convert:將 argV 轉換成 method.Type.In(index).Elem() 類型
					args = append(args, argV.Convert(method.Type.In(index).Elem()))
					continue
				}

				index = i + 1
				// 參數類型校驗
				if !argT.ConvertibleTo(method.Type.In(index)) {
					panic(fmt.Sprintf("%s can't ConvertibleTo %s", argT, method.Type.In(index)))
				}

				// 轉換成期望的參數類型
				args = append(args, argV.Convert(method.Type.In(index)))

			}
		}
        // 調用Call方法來實現對方法的調用
		method.Func.Call(args)
	} else {
		panic("無法找到此方法:" + os.Args[1])
	}
}

當然,上面只是反射的最基礎的應用,Go語言裏面的RPC就是用反射實現的,感謝趣的可以看看RPC庫的代碼,相信會對RPC會有更深的理解。

參考:

https://baike.baidu.com/item/%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章