什麼是反射?
反射的概念是由Smith在1982年首次提出的,主要是指程序可以訪問、檢測和修改它本身狀態或行爲的一種能力。應用能夠通過採用某種機制來實現對自己行爲的描述(self-representation)和監測(examination),並能根據自身行爲的狀態和結果,調整或修改應用所描述行爲的狀態和相關的語義。
其含義就是我們可以通過語言提供的反射功能,在程序運行過程中動態的調用方法、獲取並改變變量屬性,其核心就是其動態性。一般情況下,代碼邏輯在編譯時就已經確定了,變量的類型,方法調用關係都是確定的,但是有了反射,我們可以根據外部輸入,動態的調整變量類型與屬性、調用不同的方法。
Go語言反射
基本概念
Go 語言中通過 reflect
包來實現對反射的支持,reflect包中最重要的兩個方法就是 reflect.Typeof
與 reflect.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.TypeOf
與 reflect.ValueOf
的定義
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
從這兩個函數的實現,我們可以看出其參數值都是interface
,但是我們一般很少直接傳入interface類型,都是像string
,int
等的具體類型,這裏就涉及到變量類型的隱式轉換的過程:
具體類型 -> 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
提供的Set
或 SetXxx
方法來設置新的值。
反射應用
如下代碼實現了通過命令行調用對應方法的代碼:
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/