原文鏈接: http://www.limerence2017.com/2019/10/14/golang16/
反射是什么
反射其實就是通過變量動態獲取其值和類型的一種技術,有些語言是支持反射的比如python, golang,有些是不支持反射的比如C++
前文我們分析過interface的結構,無論空接口還是有方法的接口,其內部都包含type和value兩個類型,type指向了變量實際的類型
value指向了變量實際的值。而反射就是獲取這兩個類型的數據。
golang總類型分為包括 static type和concrete type. 簡單來說 static type是你在編碼是看見的類型(如int、string),
concrete type是runtime系統看見的類型
反射只能作用於interface{}類型,interface{}類型時concrete類型
下面介紹golang反射的基本用法
reflect.ValueOf與reflect.TypeOf
1 |
var num float64 = 13.14 |
golang 提供了反射功能的包reflect, reflect中ValueOf能夠將變量轉化為reflect.Value類型,reflect.TypeOf可以將變量
轉化為reflect.Type類型。
reflect.Type 表示變量的實際類型。
reflect.Value 表示變量的實際值。
reflect.Value類型提供了Kind()方法,獲取變量實際的種類。
reflect.Value類型提供了Type()方法,獲取變量實際的類型。
relfect.Type 同樣提供了Kind()方法,獲取變量的種類。
上面程序的輸出結果為
1 |
reflect type is float64 |
可見通過反射可以獲取變量的實際類型和數值,那么Kind和Type有什么區別呢?這個區別在於結構體,之后再談。如果您只需要了解
反射的基礎知識,看到這里就可以了,下面介紹反射復雜的玩法。
通過reflect.Value修改變量
如果一個變量是reflect.Value類型,則可以通過SetInt,SetFloat,SetString等方法修改變量的值,但是reflect.Value必須是
指針類型,否則無法修改原變量的值。可以通過Canset方法判斷reflect.Value是否可以修改。
另外如果reflect.Value為指針類型,需要通過Elem()解引用方可使用其方法。
我們繼續上面的例子補充代碼。
1 |
var num float64 = 13.14 |
輸出如下
1 |
reflect type is float64 |
可以看到通過rptrvalue.Elem().SetFloat(131.4)成功修改了num的數值。
而且通過打印rptrvalue的Kind為ptr指針種類,Type為float64的指針類型
注意,如果通過rptrvalue.SetFloat(131.4)會導致panic崩潰,因為此時rptrvalue為指針類型
需要通過Elem()解引用才可以使用方法。這個Elem()相當於C++編程中解引用的*
通過Interface()將relect.Value類型轉換為interface{}類型
我們可通過Interface()將relect.Value類型轉換為interface{}類型,進而轉換為原始類型。
繼續上邊的代碼,我們完善代碼
1 |
var num float64 = 13.14 |
輸出如下
1 |
reflect value is 13.14 |
可以看出rvalue.Interface().(float64)將reflect.Value類型轉換為float類型
rptrvalue.Interface().(*float64)將reflect.Value類型轉換為float指針類型
在這兩個轉換前,我們不是已經將num值修改為131.4了嗎?
為什么rvalue.Interface().(float64)轉換后還是13.14呢?
因為rvalue為值類型不是指針類型,只是存儲了num未修改前的一個副本,其值為13.14,
所以轉化為float類型值仍為13.14。
而rptrvalue為指針類型,指向的空間為num所在的空間,所以轉化為float指針后指向num所在空間。
num修改了,rptrvalue指向空間的數據就修改了。
到目前為止第一個例子就完整的寫完了。
Kind和Type有何不同?
我們先定義一個結構體Hero
1 |
type Hero struct { |
接着我們實現一個函數,通過反射判斷結構體類型
1 |
func ReflectTypeValue(itf interface{}) { |
上面函數參數為空接口,可以接受任何類型數據,內部調用了反射的TypeOf和ValueOf轉化,判斷參數類型和種類
我們在main函數測試下
1 |
ReflectTypeValue(Hero{name: "zack", id: 1}) |
輸出如下
1 |
reflect type is main.Hero |
看的出來,Hero結構體的Kind為struct,Type為main.Hero,因為Hero定義在main包,所以為main.Hero
結構體的值為{zack 1}
Hero指針的Kind為ptr,Type也為*main.Hero,值為&{zack 1}
這其實就是Kind和Type的區別,Type為具體的類型,Kind為種類,比如指針ptr,結構體struct,
整形int等。
下面我們進行更復雜的結構體遍歷和探測
通過reflect.Value的NumField函數獲取結構體成員數量
reflect.Value類型提供了NumField()函數,用來返回結構體成員數量。我們先實現一個函數遍歷探測
結構體成員。
1 |
func ReflectStructElem(itf interface{}) { |
ReflectStructElem函數首先通過reflect.ValueOf獲取reflect.Value類型,在通過reflect.Value類型
的NumField獲取實際結構體內的成員個數。
rvalue.Field(i)根據索引依次獲取結構體每個成員,elevalue類型為reflect.Value類型,所以可以通過
Kind,Type等方法獲取成員的類型和種類。
我們在主函數調用上面的方法
1 |
ReflectTypeValue(Hero{name: "zack", id: 1}) |
Hero為我們上個例子定義的結構體,輸出如下
1 |
element 0 its type is string |
可見通過reflect.Value的NumField是可以遍歷探測結構體成員的值得。
下面我們試試在main函數中加入這樣一段代碼
1 |
ReflectStructElem(&Hero{name: "Rolin", id: 20}) |
這次ReflectStructElem的參數為Hero指針類型,運行發現程序崩潰了。
我們查看下NumField的源碼
1 |
func (v Value) NumField() int { |
可以看出NumField內部判斷v必須為Struct類型,然后取出v的typ字段轉化為結構體指針進行操作。
所以NumField只能用在探測結構體類型,指針,int,float,string等類型都不能使用NumField方法。
那如果我們想遍歷結構體指針類型怎么辦?比如*Hero類型?
答案是可以的,通過Elem()解引用即可。我們再實現一個函數,用來探測結構體指針類型成員變量。
1 |
func ReflectStructPtrElem(itf interface{}) { |
ReflectStructPtrElem先通過reflect.ValueOf接口類型轉化為reflect.Value類型的rvalue,
此時rvalue實際類型為結構體指針類型,所以通過Elem()解除引用,這樣rvalue.Elem()為Hero類型。
接下來就可以遍歷和獲取結構體成員了。main函數中添加如下代碼
1 |
heroptr := &Hero{name: "zack fair", id: 2} |
輸出
1 |
element 0 its type is string |
到目前為止,我們學會了通過反射獲取基本類型(int,string,float等)的變量,也可以獲取結構體類型和
結構體指針類型的變量,以及探測其內部成員。接下來我們考慮修改結構體成員的值。
通過reflect.Value的NumMethod獲取結構體方法
我們要修改結構體成員變量的值,可以通過之前的Set方法嗎?答案是可以的,但是要求比較苛刻,要求反射的對象
是結構體指針,並且修改的成員也是指針類型才可以。而對於非指針類型的成員變量怎么修改呢?
我們先完善函數 ReflectStructPtrElem
1 |
func ReflectStructPtrElem(itf interface{}) { |
上面代碼添加了功能,獲取結構體指針類型的變量的第二個成員,判斷是否可以修改。測試下
1 |
heroptr := &Hero{name: "zack fair", id: 2} |
輸出如下
1 |
element 0 its type is string |
可見*Hero雖然為結構體指針類型,但是成員id為int類型,不可被更改。
有什么辦法更改Hero成員變量嗎?答案是有的,通過Hero的方法,我們給Hero完善幾個方法
1 |
type Hero struct { |
我們分別為Hero類增加了四個方法,兩個為Hero實現,兩個為Hero實現。通過前面幾篇文章我介紹過
Hero類型變量只能訪問基於Hero實現的方法,而Hero指針類型的變量可以訪問所有Hero方法。
包括Hero和Hero實現的所有方法。與探測結構體成員變量一樣,反射提供了方法探測和調用的api。
reflect.Value提供了MethodField方法獲取結構體實現的方法數量,
通過reflect.Value的Method方法獲取指定方法並調用。
我們實現一個函數
1 |
func ReflectStructMethod(itf interface{}) { |
對上面的函數做詳細講解
rvalue := reflect.ValueOf(itf) 將參數itf轉化為reflect.Value類型
rtype := reflect.TypeOf(itf) 將參數itf轉化為reflect.Type類型
rvalue.NumMethod 獲取結構體實現的方法個數
methodvalue := rvalue.Method(i)根據索引i獲取對應的方法,
methodvalue 為reflect.Value類型
methodtype := rtype.Method(i) 根據索引i獲取對應的方法,
methodtype 為reflect.Method類型,可以進一步獲取方法類型,名字等信息。
rvalue.Method(0).Call(nil)為方法調用,如果itf為Hero類型,
則調用了結構體的第一個方法PrintData,
Call的參數為一個reflect.Value類型的slice。這個slice存儲的就是函數調用所需的參數。
由於PrintData參數為空,所以這里傳nil就行。
params := []reflect.Value{reflect.ValueOf(“Rolin”)}構造了參數列表
如果itf為Hero類型
rvalue.Method(1).Call(params)調用了結構體實現的第二個方法SetName
那么綜上所述,其實上面的函數ReflectStructMethod功能就是遍歷結構體所實現的方法,
打印方法地址和名字,類型等信息。然后調用了打印方法和修改方法。
我們在main函數里添加代碼測試
1 |
ReflectStructMethod(Hero{name: "zack fair", id: 2}) |
輸出
1 |
method 0 value is 0x4945d0 |
可以看出每次遍歷我們打印的方法地址methodvalue都為0x4945d0,
其實這個地址就是存儲在interface的itab中的fun方法集地址。
還記得我之前剖析interface內部實現的這個圖嗎?
fun我之前說過為unitptr類型的數組,大小為1,其實這是一個柔性數組,實際大小取決於方法個數
0x4945d0就是個柔型數組的首地址。
接着打印methodtype,其實就是Method類型的結構體,包含方法名,參數,方法的索引。
方法在fun數組中是按照字符大小排序的,這個讀者可以自己gdb調試或者查看匯編源碼。
接着我們在循環外調用了打印函數PrintData()和修改函數SetName()
但是我們看到,修改函數並沒有生效。
因為SetName是基於Hero實現的,達不到修改自身屬性的目的。需要調用SetName2來修改。
SetName2是基於*Hero實現的。
為了達到修改成員變量的目的,我們在實現一個函數
1 |
func ReflectStructPtrMethod(itf interface{}) { |
ReflectStructPtrMethod 用來接收結構體指針
rvalue 實際為結構體指針類型
rvalue.NumMethod獲取結構體指針實現的方法,這其中包含結構體實現的方法和結構體指針實現的方法。
如果參數itf為*Hero類型,則NumMethod為4,包括基於Hero指針和Hero結構體實現的方法。
這里可能有讀者會問如果rvalue為指針類型為什么不需要用Elem()解引用再調用NumMethod?
我們看下NumMethod的方法源碼
1 |
func (v Value) NumMethod() int { |
可見NumMethod和NumField不同,並沒有要求v為結構體類型,所以結構體指針也能調用NumMethod。
只是結構體指針和結構體調用NumMethod會返回不同的數量,比如rvalue實際類型為*Hero類型,
rvalue.Elem().NumMethod()解引用返回值為2,因為Hero實現了PrintData和SetName方法。
我們調用了方法進行修改,然后為了測試解引用和不解引用調用NumMethod的區別,分別進行了打印。
在main函數中測試
1 |
Hero pointer struct method list...................... |
可以看到Hero指針的方法個數為4個,Hero結構體方法個數為2個,並且通過調用SetName2方法
達到修改成員變量的目的了。
通過方法名獲取方法
reflect.Value提供了MethodByName方法,根據名字獲取具體方法
我們寫一個函數,獲取方法並調用
1 |
func GetMethodByName(itf interface{}) { |
rvalue.MethodByName(“PrintData2”)獲取名字為PrintData2的方法。
methodvalue.IsValid() 判斷是否獲取成功。
methodvalue.Call(nil) 調用方法。
我們在main函數里調用這個函數測試下
1 |
GetMethodByName(&Hero{name: "zack fair", id: 2}) |
輸出如下
1 |
Hero name is zack fair id is 2 |
可見通過名字獲取方法並調用,也能達到修改Hero成員屬性的目的。
讀者可以在main函數中添加如下代碼,測試下,看看效果,並想想為什么
1 |
GetMethodByName(Hero{name: "Itach", id: 20}) |
總結
本文提供了golang反射包的基本api和方法,講述了如何使用reflect包動態獲取參數類型和種類,
以及結構體和機構體指針成員等。接着我們討論了方法的獲取和調用。反射是golang提供的強大
功能,可以動態獲取和判斷類型,調用成員方法等。
反射是一把雙刃劍,提供動態獲取類型和動態調用的強大功能,同時也會造成程序效率的衰退,
因為反射是通過類型轉換和循環遍歷探測其類型達到的,建議適度使用,在能確定類型時盡量用
interface進行轉換。
感謝關注我的公眾號
