golang(11) 反射用法詳解


原文鏈接: 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
2
3
4
5
6
7
8
   var num float64 = 13.14
rtype := reflect.TypeOf(num)
fmt.Println("reflect type is ", rtype)
rvalue := reflect.ValueOf(num)
fmt.Println("reflect value is ", rvalue)
fmt.Println("reflect value kind is", rvalue.Kind())
fmt.Println("reflect type kind is", rtype.Kind())
fmt.Println("reflect value type is", rvalue.Type())

golang 提供了反射功能的包reflect, reflect中ValueOf能夠將變量轉化為reflect.Value類型,reflect.TypeOf可以將變量
轉化為reflect.Type類型。

reflect.Type 表示變量的實際類型。
reflect.Value 表示變量的實際值。

reflect.Value類型提供了Kind()方法,獲取變量實際的種類。
reflect.Value類型提供了Type()方法,獲取變量實際的類型。
relfect.Type 同樣提供了Kind()方法,獲取變量的種類。
上面程序的輸出結果為

1
2
3
4
5
reflect type is float64
reflect value is 13.14
reflect value kind is float64
reflect type kind is float64
reflect value type is float64

 

可見通過反射可以獲取變量的實際類型和數值,那么Kind和Type有什么區別呢?這個區別在於結構體,之后再談。如果您只需要了解
反射的基礎知識,看到這里就可以了,下面介紹反射復雜的玩法。

通過reflect.Value修改變量

如果一個變量是reflect.Value類型,則可以通過SetInt,SetFloat,SetString等方法修改變量的值,但是reflect.Value必須是
指針類型,否則無法修改原變量的值。可以通過Canset方法判斷reflect.Value是否可以修改。
另外如果reflect.Value為指針類型,需要通過Elem()解引用方可使用其方法。
我們繼續上面的例子補充代碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var num float64 = 13.14
rtype := reflect.TypeOf(num)
fmt.Println("reflect type is ", rtype)
rvalue := reflect.ValueOf(num)
fmt.Println("reflect value is ", rvalue)
fmt.Println("reflect value kind is", rvalue.Kind())
fmt.Println("reflect type kind is", rtype.Kind())
fmt.Println("reflect value type is", rvalue.Type())

rptrvalue := reflect.ValueOf(&num)
fmt.Println("reflect value is ", rptrvalue)
fmt.Println("reflect value kind is", rptrvalue.Kind())
fmt.Println("reflect type kind is", rptrvalue.Kind())
fmt.Println("reflect value type is", rptrvalue.Type())
if rptrvalue.Elem().CanSet() {
rptrvalue.Elem().SetFloat(131.4)
}

fmt.Println(num)

 

輸出如下

1
2
3
4
5
6
7
8
9
10
reflect type is float64
reflect value is 13.14
reflect value kind is float64
reflect type kind is float64
reflect value type is float64
reflect value is 0xc000012098
reflect value kind is ptr
reflect type kind is ptr
reflect value type is *float64
131.4

 

可以看到通過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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var num float64 = 13.14
rtype := reflect.TypeOf(num)
fmt.Println("reflect type is ", rtype)
rvalue := reflect.ValueOf(num)
fmt.Println("reflect value is ", rvalue)
fmt.Println("reflect value kind is", rvalue.Kind())
fmt.Println("reflect type kind is", rtype.Kind())
fmt.Println("reflect value type is", rvalue.Type())

rptrvalue := reflect.ValueOf(&num)
fmt.Println("reflect value is ", rptrvalue)
fmt.Println("reflect value kind is", rptrvalue.Kind())
fmt.Println("reflect type kind is", rptrvalue.Kind())
fmt.Println("reflect value type is", rptrvalue.Type())
if rptrvalue.Elem().CanSet() {
rptrvalue.Elem().SetFloat(131.4)
}

fmt.Println(num)
//rvalue 為reflect包的Value類型
//可通過Interface()轉化為interface{}類型,進而轉化為原始類型
rawvalue := rvalue.Interface().(float64)
fmt.Println("rawvalue is ", rawvalue)

rawptrvalue := rptrvalue.Interface().(*float64)
fmt.Println("rawptrvalue is ", *rawptrvalue)

 

輸出如下

1
2
3
4
5
6
7
8
9
10
11
reflect value is  13.14
reflect value kind is float64
reflect type kind is float64
reflect value type is float64
reflect value is 0xc000012098
reflect value kind is ptr
reflect type kind is ptr
reflect value type is *float64
131.4
rawvalue is 13.14
rawptrvalue is 131.4

 

可以看出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
2
3
4
type Hero struct {
name string
id int
}

 

接着我們實現一個函數,通過反射判斷結構體類型

1
2
3
4
5
6
7
8
9
10
func ReflectTypeValue(itf interface{}) {

rtype := reflect.TypeOf(itf)
fmt.Println("reflect type is ", rtype)
rvalue := reflect.ValueOf(itf)
fmt.Println("reflect value is ", rvalue)
fmt.Println("reflect value kind is", rvalue.Kind())
fmt.Println("reflect type kind is", rtype.Kind())
fmt.Println("reflect value type is", rvalue.Type())
}

 

上面函數參數為空接口,可以接受任何類型數據,內部調用了反射的TypeOf和ValueOf轉化,判斷參數類型和種類
我們在main函數測試下

1
2
ReflectTypeValue(Hero{name: "zack", id: 1})
ReflectTypeValue(&Hero{name: "zack", id: 1})

 

輸出如下

1
2
3
4
5
6
7
8
9
10
reflect type is main.Hero
reflect value is {zack 1}
reflect value kind is struct
reflect type kind is struct
reflect value type is main.Hero
reflect type is *main.Hero
reflect value is &{zack 1}
reflect value kind is ptr
reflect type kind is ptr
reflect value 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
2
3
4
5
6
7
8
9
	func ReflectStructElem(itf interface{}) {
rvalue := reflect.ValueOf(itf)
for i := 0; i < rvalue.NumField(); i++ {
elevalue := rvalue.Field(i)
fmt.Println("element ", i, " its type is ", elevalue.Type())
fmt.Println("element ", i, " its kind is ", elevalue.Kind())
fmt.Println("element ", i, " its value is ", elevalue)
}
}

 

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
2
3
4
5
6
element  0 its type is string
element 0 its kind is string
element 0 its value is zack fair
element 1 its type is int
element 1 its kind is int
element 1 its value is 2

 

可見通過reflect.Value的NumField是可以遍歷探測結構體成員的值得。
下面我們試試在main函數中加入這樣一段代碼

1
ReflectStructElem(&Hero{name: "Rolin", id: 20})

 

這次ReflectStructElem的參數為Hero指針類型,運行發現程序崩潰了。
我們查看下NumField的源碼

1
2
3
4
5
func (v Value) NumField() int {
v.mustBe(Struct)
tt := (*structType)(unsafe.Pointer(v.typ))
return len(tt.fields)
}

 

可以看出NumField內部判斷v必須為Struct類型,然后取出v的typ字段轉化為結構體指針進行操作。
所以NumField只能用在探測結構體類型,指針,int,float,string等類型都不能使用NumField方法。
那如果我們想遍歷結構體指針類型怎么辦?比如*Hero類型?
答案是可以的,通過Elem()解引用即可。我們再實現一個函數,用來探測結構體指針類型成員變量。

1
2
3
4
5
6
7
8
9
func ReflectStructPtrElem(itf interface{}) {
rvalue := reflect.ValueOf(itf)
for i := 0; i < rvalue.Elem().NumField(); i++ {
elevalue := rvalue.Elem().Field(i)
fmt.Println("element ", i, " its type is ", elevalue.Type())
fmt.Println("element ", i, " its kind is ", elevalue.Kind())
fmt.Println("element ", i, " its value is ", elevalue)
}
}

 

ReflectStructPtrElem先通過reflect.ValueOf接口類型轉化為reflect.Value類型的rvalue,
此時rvalue實際類型為結構體指針類型,所以通過Elem()解除引用,這樣rvalue.Elem()為Hero類型。
接下來就可以遍歷和獲取結構體成員了。main函數中添加如下代碼

1
2
heroptr := &Hero{name: "zack fair", id: 2}
ReflectStructPtrElem(heroptr)

 

輸出

1
2
3
4
5
6
element  0 its type is string
element 0 its kind is string
element 0 its value is zack fair
element 1 its type is int
element 1 its kind is int
element 1 its value is 2

 

到目前為止,我們學會了通過反射獲取基本類型(int,string,float等)的變量,也可以獲取結構體類型和
結構體指針類型的變量,以及探測其內部成員。接下來我們考慮修改結構體成員的值。

通過reflect.Value的NumMethod獲取結構體方法

我們要修改結構體成員變量的值,可以通過之前的Set方法嗎?答案是可以的,但是要求比較苛刻,要求反射的對象
是結構體指針,並且修改的成員也是指針類型才可以。而對於非指針類型的成員變量怎么修改呢?
我們先完善函數 ReflectStructPtrElem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ReflectStructPtrElem(itf interface{}) {
rvalue := reflect.ValueOf(itf)
for i := 0; i < rvalue.Elem().NumField(); i++ {
elevalue := rvalue.Elem().Field(i)
fmt.Println("element ", i, " its type is ", elevalue.Type())
fmt.Println("element ", i, " its kind is ", elevalue.Kind())
fmt.Println("element ", i, " its value is ", elevalue)
}

if rvalue.Elem().Field(1).CanSet() {
rvalue.Elem().Field(1).SetInt(100)
} else {
fmt.Println("struct element 1 can't be changed")
}

}

 

上面代碼添加了功能,獲取結構體指針類型的變量的第二個成員,判斷是否可以修改。測試下

1
2
heroptr := &Hero{name: "zack fair", id: 2}
ReflectStructPtrElem(heroptr)

 

輸出如下

1
2
3
4
5
6
7
element  0 its type is string
element 0 its kind is string
element 0 its value is zack fair
element 1 its type is int
element 1 its kind is int
element 1 its value is 2
struct element 1 can't be changed

 

可見*Hero雖然為結構體指針類型,但是成員id為int類型,不可被更改。
有什么辦法更改Hero成員變量嗎?答案是有的,通過Hero的方法,我們給Hero完善幾個方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Hero struct {
name string
id int
}

func (h Hero) PrintData() {
fmt.Println("Hero name is ", h.name, " id is ", h.id)
}

func (h Hero) SetName(name string) {
h.name = name
}

func (h *Hero) SetName2(name string) {
h.name = name
}

func (h *Hero) PrintData2() {
fmt.Println("Hero name is ", h.name, " id is ", h.id)
}

 

我們分別為Hero類增加了四個方法,兩個為Hero實現,兩個為Hero實現。通過前面幾篇文章我介紹過
Hero類型變量只能訪問基於Hero實現的方法,而Hero指針類型的變量可以訪問所有Hero方法。
包括Hero和
Hero實現的所有方法。與探測結構體成員變量一樣,反射提供了方法探測和調用的api。
reflect.Value提供了MethodField方法獲取結構體實現的方法數量,
通過reflect.Value的Method方法獲取指定方法並調用。
我們實現一個函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func ReflectStructMethod(itf interface{}) {
rvalue := reflect.ValueOf(itf)
rtype := reflect.TypeOf(itf)
for i := 0; i < rvalue.NumMethod(); i++ {
methodvalue := rvalue.Method(i)
fmt.Println("method ", i, " value is ", methodvalue)
methodtype := rtype.Method(i)
fmt.Println("method ", i, " type is ", methodtype)
fmt.Println("method ", i, " name is ", methodtype.Name)
fmt.Println("method ", i, " method.type is ", methodtype.Type)
}

//reflect.ValueOf 方法調用,無參方法調用
fmt.Println(rvalue.Method(0).Call(nil))
//有參方法調用
params := []reflect.Value{reflect.ValueOf("Rolin")}
fmt.Println(rvalue.Method(1).Call(params))
//雖然修改了,但是並沒有生效
fmt.Println(rvalue.Method(0).Call(nil))
}

 

對上面的函數做詳細講解
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
2
3
4
5
6
7
8
9
10
method  0 value is 0x4945d0
method 0 type is {PrintData func(main.Hero) <func(main.Hero) Value> 0}
method 0 name is PrintData
method 0 method.type is func(main.Hero)
method 1 value is 0x4945d0
method 1 type is {SetName func(main.Hero, string) <func(main.Hero, string) Value> 1}
method 1 name is SetName
method 1 method.type is func(main.Hero, string)
Hero name is zack fair id is 2
Hero name is zack fair id is 2

 

可以看出每次遍歷我們打印的方法地址methodvalue都為0x4945d0,
其實這個地址就是存儲在interface的itab中的fun方法集地址。
還記得我之前剖析interface內部實現的這個圖嗎?
3.jpg
fun我之前說過為unitptr類型的數組,大小為1,其實這是一個柔性數組,實際大小取決於方法個數
0x4945d0就是個柔型數組的首地址。
接着打印methodtype,其實就是Method類型的結構體,包含方法名,參數,方法的索引。
方法在fun數組中是按照字符大小排序的,這個讀者可以自己gdb調試或者查看匯編源碼。
接着我們在循環外調用了打印函數PrintData()和修改函數SetName()
但是我們看到,修改函數並沒有生效。
因為SetName是基於Hero實現的,達不到修改自身屬性的目的。需要調用SetName2來修改。
SetName2是基於*Hero實現的。
為了達到修改成員變量的目的,我們在實現一個函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func ReflectStructPtrMethod(itf interface{}) {
rvalue := reflect.ValueOf(itf)
rtype := reflect.TypeOf(itf)
fmt.Println("Hero pointer struct method list......................")
for i := 0; i < rvalue.NumMethod(); i++ {
methodvalue := rvalue.Method(i)
fmt.Println("method ", i, " value is ", methodvalue)
methodtype := rtype.Method(i)
fmt.Println("method ", i, " type is ", methodtype)
fmt.Println("method ", i, " name is ", methodtype.Name)
fmt.Println("method ", i, " method.type is ", methodtype.Type)
}

//reflect.ValueOf 方法調用,無參方法調用
fmt.Println(rvalue.Method(1).Call(nil))
//有參方法調用
params := []reflect.Value{reflect.ValueOf("Rolin")}
fmt.Println(rvalue.Method(3).Call(params))
//修改了,生效
fmt.Println(rvalue.Method(0).Call(nil))

fmt.Println("Hero Struct method list......................")
for i := 0; i < rvalue.Elem().NumMethod(); i++ {
methodvalue := rvalue.Elem().Method(i)
fmt.Println("method ", i, " value is ", methodvalue)
methodtype := rtype.Elem().Method(i)
fmt.Println("method ", i, " type is ", methodtype)
fmt.Println("method ", i, " name is ", methodtype.Name)
fmt.Println("method ", i, " method.type is ", methodtype.Type)
}
}

 

ReflectStructPtrMethod 用來接收結構體指針
rvalue 實際為結構體指針類型
rvalue.NumMethod獲取結構體指針實現的方法,這其中包含結構體實現的方法和結構體指針實現的方法。
如果參數itf為*Hero類型,則NumMethod為4,包括基於Hero指針和Hero結構體實現的方法。
這里可能有讀者會問如果rvalue為指針類型為什么不需要用Elem()解引用再調用NumMethod?
我們看下NumMethod的方法源碼

1
2
3
4
5
6
7
8
9
func (v Value) NumMethod() int {
if v.typ == nil {
panic(&ValueError{"reflect.Value.NumMethod", Invalid})
}
if v.flag&flagMethod != 0 {
return 0
}
return v.typ.NumMethod()
}

 

可見NumMethod和NumField不同,並沒有要求v為結構體類型,所以結構體指針也能調用NumMethod。
只是結構體指針和結構體調用NumMethod會返回不同的數量,比如rvalue實際類型為*Hero類型,
rvalue.Elem().NumMethod()解引用返回值為2,因為Hero實現了PrintData和SetName方法。
我們調用了方法進行修改,然后為了測試解引用和不解引用調用NumMethod的區別,分別進行了打印。
在main函數中測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Hero pointer struct method list......................
method 0 value is 0x4945d0
method 0 type is {PrintData func(*main.Hero) <func(*main.Hero) Value> 0}
method 0 name is PrintData
method 0 method.type is func(*main.Hero)
method 1 value is 0x4945d0
method 1 type is {PrintData2 func(*main.Hero) <func(*main.Hero) Value> 1}
method 1 name is PrintData2
method 1 method.type is func(*main.Hero)
method 2 value is 0x4945d0
method 2 type is {SetName func(*main.Hero, string) <func(*main.Hero, string) Value> 2}
method 2 name is SetName
method 2 method.type is func(*main.Hero, string)
method 3 value is 0x4945d0
method 3 type is {SetName2 func(*main.Hero, string) <func(*main.Hero, string) Value> 3}
method 3 name is SetName2
method 3 method.type is func(*main.Hero, string)
Hero name is zack fair id is 2
Hero name is Rolin id is 2
Hero Struct method list......................
method 0 value is 0x4945d0
method 0 type is {PrintData func(main.Hero) <func(main.Hero) Value> 0}
method 0 name is PrintData
method 0 method.type is func(main.Hero)
method 1 value is 0x4945d0
method 1 type is {SetName func(main.Hero, string) <func(main.Hero, string) Value> 1}
method 1 name is SetName
method 1 method.type is func(main.Hero, string)

 

可以看到Hero指針的方法個數為4個,Hero結構體方法個數為2個,並且通過調用SetName2方法
達到修改成員變量的目的了。

通過方法名獲取方法

reflect.Value提供了MethodByName方法,根據名字獲取具體方法
我們寫一個函數,獲取方法並調用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func GetMethodByName(itf interface{}) {
rvalue := reflect.ValueOf(itf)
methodvalue := rvalue.MethodByName("PrintData2")
if !methodvalue.IsValid() {
return
}

methodvalue.Call(nil)

methodset := rvalue.MethodByName("SetName2")
if !methodset.IsValid() {
return
}
params := []reflect.Value{reflect.ValueOf("Hurricane")}
methodset.Call(params)

methodvalue.Call(nil)
}

 

rvalue.MethodByName(“PrintData2”)獲取名字為PrintData2的方法。
methodvalue.IsValid() 判斷是否獲取成功。
methodvalue.Call(nil) 調用方法。
我們在main函數里調用這個函數測試下

1
GetMethodByName(&Hero{name: "zack fair", id: 2})

 

輸出如下

1
2
Hero name is  zack fair  id is  2
Hero name is Hurricane id is 2

 

可見通過名字獲取方法並調用,也能達到修改Hero成員屬性的目的。
讀者可以在main函數中添加如下代碼,測試下,看看效果,並想想為什么

1
GetMethodByName(Hero{name: "Itach", id: 20})

 

總結

本文提供了golang反射包的基本api和方法,講述了如何使用reflect包動態獲取參數類型和種類,
以及結構體和機構體指針成員等。接着我們討論了方法的獲取和調用。反射是golang提供的強大
功能,可以動態獲取和判斷類型,調用成員方法等。
反射是一把雙刃劍,提供動態獲取類型和動態調用的強大功能,同時也會造成程序效率的衰退,
因為反射是通過類型轉換和循環遍歷探測其類型達到的,建議適度使用,在能確定類型時盡量用
interface進行轉換。
感謝關注我的公眾號
wxgzh.jpg


免責聲明!

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



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