學習使用 Go 的反射

什麼是反射

大多數時候,Go中的變量,類型和函數很是簡單直接。當須要一個類型、變量或者是函數時,能夠直接定義它們:golang

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 反射的實現。bash

反射變量類型

首先讓咱們看一下類型。你可使用反射來調用函數varType := reflect.TypeOf(var)來獲取變量var的類型。這將返回類型爲reflect.Type的變量,該變量具備獲取定義時變量的類型的各類信息的方法集。下面咱們來看一下經常使用的獲取類型信息的方法。網絡

咱們要看的第一個方法是Name()。這將返回變量類型的名稱。某些類型(例如切片或指針)沒有名稱,此方法會返回空字符串。閉包

下一個方法,也是我認爲第一個真正很是有用的方法是Kind()。Type是由Kind組成的---Kind 是切片,映射,指針,結構,接口,字符串,數組,函數,int或其餘某種原始類型的抽象表示。要理解Type和Kind之間的差別可能有些棘手,可是請你以這種方式來思考。若是定義一個名爲Foo的結構體,則Kind爲struct,類型爲Foo。app

使用反射時要注意的一件事:反射包中的全部內容都假定你知道本身在作什麼,而且若是使用不正確,許多函數和方法調用都會引發 panic。例如,若是你在reflect.Type上調用與當前類型不一樣的類型關聯的方法,您的代碼將會panic函數

若是變量是指針,映射,切片,通道或數組變量,則可使用varType.Elem()找出指向或包含的值的類型。工具

若是變量是結構體,則可使用反射來獲取結構體中的字段數,並從每一個字段上獲取reflect.StructField結構體。 reflection.StructField爲您提供了字段的名稱,標號,類型和結構體標籤。其中標籤信息對應reflect.StructTag類型的字符串,而且它提供了Get方法用於解析和根據特定key提取標籤信息中的子串。ui

下面是一個簡單的示例,用於輸出各類變量的類型信息:spa

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: play.golang.org/p/lZ97yAUHx…

使用反射建立新實例

除了檢查變量的類型外,還可使用反射來讀取,設置或建立值。首先,須要使用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 play.golang.org/p/PFcEYfZqZ…

反射建立引用類型的實例

除了生成內置類型和用戶定義類型的實例以外,還可使用反射來生成一般須要make函數的實例。可使用reflect.MakeSlicereflect.MakeMapreflect.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開發人員都應瞭解並學會的強大工具。可是使用他們能夠用來作什麼呢?在下一篇博客文章中,我將探討現有庫中反射的一些用法,並使用反射來建立一些新的東西。

相關文章
相關標籤/搜索