什么是反射
大多數時候,Go中的變量,類型和函數非常簡單直接。當需要一個類型、變量或者是函數時,可以直接定義它們:
type Foo struct {
A int
B string
}
var x Foo
func DoSomething(f Foo) {
fmt.Println(f.A, f.B)
}
但是有時你希望在運行時使用變量的在編寫程序時還不存在的信息。比如你正在嘗試將文件或網絡請求中的數據映射到變量中。或者你想構建一個適用于不同類型的工具。在這種情況下,你需要使用反射。反射使您能夠在運行時檢查類型。它還允許您在運行時檢查,修改和創建變量,函數和結構體。
Go中的反射是基于三個概念構建的:類型,種類和值(Types Kinds Values)。標準庫中的reflect包提供了 Go 反射的實現。
反射變量類型
首先讓我們看一下類型。你可以使用反射來調用函數varType := reflect.TypeOf(var)來獲取變量var的類型。這將返回類型為reflect.Type的變量,該變量具有獲取定義時變量的類型的各種信息的方法集。下面我們來看一下常用的獲取類型信息的方法。
我們要看的第一個方法是Name()。這將返回變量類型的名稱。某些類型(例如切片或指針)沒有名稱,此方法會返回空字符串。
下一個方法,也是我認為第一個真正非常有用的方法是Kind()。Type是由Kind組成的---Kind 是切片,映射,指針,結構,接口,字符串,數組,函數,int或其他某種原始類型的抽象表示。要理解Type和Kind之間的差異可能有些棘手,但是請你以這種方式來思考。如果定義一個名為Foo的結構體,則Kind為struct,類型為Foo。
使用反射時要注意的一件事:反射包中的所有內容都假定你知道自己在做什么,并且如果使用不正確,許多函數和方法調用都會引起 panic。例如,如果你在reflect.Type上調用與當前類型不同的類型關聯的方法,您的代碼將會panic。
如果變量是指針,映射,切片,通道或數組變量,則可以使用varType.Elem()找出指向或包含的值的類型。
如果變量是結構體,則可以使用反射來獲取結構體中的字段數,并從每個字段上獲取reflect.StructField結構體。 reflection.StructField為您提供了字段的名稱,標號,類型和結構體標簽。其中標簽信息對應reflect.StructTag類型的字符串,并且它提供了Get方法用于解析和根據特定key提取標簽信息中的子串。
下面是一個簡單的示例,用于輸出各種變量的類型信息:
type Foo struct {
A int `tag1:"First Tag" tag2:"Second Tag"`
B string
}
func main() {
sl := []int{1, 2, 3}
greeting := "hello"
greetingPtr := greeting
f := Foo{A: 10, B: "Salutations"}
fp := f
slType := reflect.TypeOf(sl)
gType := reflect.TypeOf(greeting)
grpType := reflect.TypeOf(greetingPtr)
fType := reflect.TypeOf(f)
fpType := reflect.TypeOf(fp)
examiner(slType, 0)
examiner(gType, 0)
examiner(grpType, 0)
examiner(fType, 0)
examiner(fpType, 0)
}
func examiner(t reflect.Type, depth int) {
fmt.Println(strings.Repeat("\t", depth), "Type is", t.Name(), "and kind is", t.Kind())
switch t.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice:
fmt.Println(strings.Repeat("\t", depth+1), "Contained type:")
examiner(t.Elem(), depth+1)
case reflect.Struct:
for i := 0; i t.NumField(); i++ {
f := t.Field(i)
fmt.Println(strings.Repeat("\t", depth+1), "Field", i+1, "name is", f.Name, "type is", f.Type.Name(), "and kind is", f.Type.Kind())
if f.Tag != "" {
fmt.Println(strings.Repeat("\t", depth+2), "Tag is", f.Tag)
fmt.Println(strings.Repeat("\t", depth+2), "tag1 is", f.Tag.Get("tag1"), "tag2 is", f.Tag.Get("tag2"))
}
}
}
}
變量的類型輸出如下:
Type is and kind is slice
Contained type:
Type is int and kind is int
Type is string and kind is string
Type is and kind is ptr
Contained type:
Type is string and kind is string
Type is Foo and kind is struct
Field 1 name is A type is int and kind is int
Tag is tag1:"First Tag" tag2:"Second Tag"
tag1 is First Tag tag2 is Second Tag
Field 2 name is B type is string and kind is string
Type is and kind is ptr
Contained type:
Type is Foo and kind is struct
Field 1 name is A type is int and kind is int
Tag is tag1:"First Tag" tag2:"Second Tag"
tag1 is First Tag tag2 is Second Tag
Field 2 name is B type is string and kind is string
Run in go playground: https://play.golang.org/p/lZ97yAUHxX
使用反射創建新實例
除了檢查變量的類型外,還可以使用反射來讀取,設置或創建值。首先,需要使用refVal := reflect.ValueOf(var) 為變量創建一個reflect.Value實例。如果希望能夠使用反射來修改值,則必須使用refPtrVal := reflect.ValueOf(&var);獲得指向變量的指針。如果不這樣做,則可以使用反射來讀取該值,但不能對其進行修改。
一旦有了reflect.Value實例就可以使用Type()方法獲取變量的reflect.Type。
如果要修改值,請記住它必須是一個指針,并且必須首先對其進行解引用。使用refPtrVal.Elem().Set(newRefVal)來修改值,并且傳遞給Set()的值也必須是reflect.Value。
如果要創建一個新值,可以使用函數newPtrVal := reflect.New(varType)來實現,并傳入一個reflect.Type。這將返回一個指針值,然后可以像上面那樣使用Elem().Set()對其進行修改。
最后,你可以通過調用Interface()方法從reflect.Value回到普通變量值。由于Go沒有泛型,因此變量的原始類型會丟失;該方法返回類型為interface{}的值。如果創建了一個指針以便可以修改該值,則需要使用Elem().Interface()解引用反射的指針。在這兩種情況下,都需要將空接口轉換為實際類型才能使用它。
下面的代碼來演示這些概念:
type Foo struct {
A int `tag1:"First Tag" tag2:"Second Tag"`
B string
}
func main() {
greeting := "hello"
f := Foo{A: 10, B: "Salutations"}
gVal := reflect.ValueOf(greeting)
// not a pointer so all we can do is read it
fmt.Println(gVal.Interface())
gpVal := reflect.ValueOf(greeting)
// it's a pointer, so we can change it, and it changes the underlying variable
gpVal.Elem().SetString("goodbye")
fmt.Println(greeting)
fType := reflect.TypeOf(f)
fVal := reflect.New(fType)
fVal.Elem().Field(0).SetInt(20)
fVal.Elem().Field(1).SetString("Greetings")
f2 := fVal.Elem().Interface().(Foo)
fmt.Printf("%+v, %d, %s\n", f2, f2.A, f2.B)
}
他們的輸出如下:
hello
goodbye
{A:20 B:Greetings}, 20, Greetings
Run in go playground https://play.golang.org/p/PFcEYfZqZ8
反射創建引用類型的實例
除了生成內置類型和用戶定義類型的實例之外,還可以使用反射來生成通常需要make函數的實例。可以使用reflect.MakeSlice,reflect.MakeMap和reflect.MakeChan函數制作切片,Map或通道。在所有情況下,都提供一個reflect.Type,然后獲取一個reflect.Value,可以使用反射對其進行操作,或者可以將其分配回一個標準變量。
func main() {
// 定義變量
intSlice := make([]int, 0)
mapStringInt := make(map[string]int)
// 獲取變量的 reflect.Type
sliceType := reflect.TypeOf(intSlice)
mapType := reflect.TypeOf(mapStringInt)
// 使用反射創建類型的新實例
intSliceReflect := reflect.MakeSlice(sliceType, 0, 0)
mapReflect := reflect.MakeMap(mapType)
// 將創建的新實例分配回一個標準變量
v := 10
rv := reflect.ValueOf(v)
intSliceReflect = reflect.Append(intSliceReflect, rv)
intSlice2 := intSliceReflect.Interface().([]int)
fmt.Println(intSlice2)
k := "hello"
rk := reflect.ValueOf(k)
mapReflect.SetMapIndex(rk, rv)
mapStringInt2 := mapReflect.Interface().(map[string]int)
fmt.Println(mapStringInt2)
}
使用反射創建函數
反射不僅僅可以為存儲數據創造新的地方。還可以使用reflect.MakeFunc函數使用reflect來創建新函數。該函數期望我們要創建的函數的reflect.Type,以及一個閉包,其輸入參數為[]reflect.Value類型,其返回類型也為[] reflect.Value類型。下面是一個簡單的示例,它為傳遞給它的任何函數創建一個定時包裝器:
func MakeTimedFunction(f interface{}) interface{} {
rf := reflect.TypeOf(f)
if rf.Kind() != reflect.Func {
panic("expects a function")
}
vf := reflect.ValueOf(f)
wrapperF := reflect.MakeFunc(rf, func(in []reflect.Value) []reflect.Value {
start := time.Now()
out := vf.Call(in)
end := time.Now()
fmt.Printf("calling %s took %v\n", runtime.FuncForPC(vf.Pointer()).Name(), end.Sub(start))
return out
})
return wrapperF.Interface()
}
func timeMe() {
fmt.Println("starting")
time.Sleep(1 * time.Second)
fmt.Println("ending")
}
func timeMeToo(a int) int {
fmt.Println("starting")
time.Sleep(time.Duration(a) * time.Second)
result := a * 2
fmt.Println("ending")
return result
}
func main() {
timed := MakeTimedFunction(timeMe).(func())
timed()
timedToo := MakeTimedFunction(timeMeToo).(func(int) int)
fmt.Println(timedToo(2))
}
你可以在goplayground運行代碼https://play.golang.org/p/QZ8ttFZzGx并看到輸出如下:
starting
ending
calling main.timeMe took 1s
starting
ending
calling main.timeMeToo took 2s
4
反射是每個Go開發人員都應了解并學會的強大工具。但是使用他們可以用來做什么呢?在下一篇博客文章中,我將探討現有庫中反射的一些用法,并使用反射來創建一些新的東西。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- golang之反射和斷言的具體使用
- Go系列教程之反射的用法
- 詳解Golang利用反射reflect動態調用方法
- 淺談Go語言中的結構體struct & 接口Interface & 反射
- Go語言學習筆記之反射用法詳解
- Go語言中反射的正確使用
- 談談Go語言的反射三定律
- go語言通過反射獲取和設置結構體字段值的方法
- Go語言中使用反射的方法