Go 譯文之如何使用反射

做者:Jon Bodner | 地址:Learning to Use Go Reflectiongit

什麼是反射

多數狀況下,Go 中的變量、類型和函數的使用都是很是簡單的。github

當你須要一個類型,定義以下:golang

type Foo struct {
  A int
  B string
}
複製代碼

當你須要一個變量,定義以下:編程

var x Foo
複製代碼

當你須要一個函數,定義以下:api

func DoSomething(f Foo) {
  fmt.Println(f.A, f.B)
}
複製代碼

但有時候,你想使用的變量依賴於運行時信息,它們在編程時並不存在。好比數據來源於文件,或來源於網絡,你想把它映射到一個變量,而它們多是不一樣的類型。在這類場景下,你就須要用到反射。反射讓你能夠在運行時檢查類型,建立、更新、檢查變量以及組織結構。數組

Go 中的反射主要圍繞着三個概念:類型(Types)、類別(Kinds)和值(Values)。反射的實現源碼位於 Go 標準庫 reflection 包中。bash

檢查類型

首先,讓咱們來看看類型(Types)。你能夠經過 reflect.TypeOf(var) 形式的函數調用獲取變量的類型,它會返回一個類型爲 reflect.Type 的變量,reflect.Type 中的操做方法涉及了定義該類型變量的各種信息。微信

咱們要看的第一個方法是 Name(),它返回的是類型的名稱。有些類型,好比 slice 或 指針,沒有類型名稱,那麼將會返回空字符串。網絡

下一個介紹方法是 Kind(),個人觀點,這是第一個真正有用的方法。Kind,即類別,好比切片 slice、映射 map、指針 pointer、結構體 struct、接口 interface、字符串 string、數組 array、函數 function、整型 int、或其餘的基本類型。type 和 kind 是區別不是那麼容易理清楚,可是能夠這麼想:閉包

當你定義一個名稱爲 Foo 的結構體,那麼它的 kind 是 struct,而它的 type 是 Foo。

當使用反射時,咱們必需要意識到:在使用 reflect 包時,會假設你清楚的知道本身在作什麼,若是使用不當,將會產生 panic。舉個例子,你在 int 類型上調用 struct 結構體類型上才用的方法,你的代碼就會產生 panic。咱們時刻要記住,什麼類型有有什麼方法可使用,從而避免產生 panic。

若是一個變量是指針、映射、切片、管道、或者數組類型,那麼這個變量的類型就能夠調用方法 varType.Elem()。

若是一個變量是結構體,那麼你就可使用反射去獲得它的字段個數,而且能夠獲得每一個字段的信息,這些信息包含在 reflect.StructField 結構體中。reflect.StructField 包含字段的名稱、排序、類型、標籤。

前言萬語也不如一行代碼看的明白,下面的這個例子輸出了不一樣變量所屬類型的信息。

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
複製代碼

運行示例

建立實例

除了檢查變量的類型外,你還能夠利用來獲取、設置和建立變量。首先,經過 refVal := reflect.ValueOf(var) 建立類型爲 reflect.Value 的實例。若是你想經過反射來更新值,那麼必需要獲取到變量的指針 refPtrVal := reflect.ValueOf(&var),若是不這麼作,那麼你只能讀取值,而不能設置值。

一旦獲得變量的 reflect.Value,你就能夠經過 Value 的 Type 屬性獲取變量的 reflect.Type 類型信息。

若是想更新值,記住要經過指針,並且在設置時,要先取消引用,經過 refPtrVal.Elem().Set(newRefVal) 更新其中的值,傳遞給 Set 的參數也必需要是 reflect.Value 類型。

若是想建立一個新的變量,能夠經過 reflect.New(varType) 實現,傳遞的參數是 reflect.Type 類型,該方法將會返回一個指針,如前面介紹的那樣,你能夠經過使用 Elem().Set() 來設置它的值。

最終,經過 Interface() 方法,你就獲得一個正常的變量。Go 中沒有泛型,變量的類型將會丟失,Interface() 方法將會返回一個類型爲 interface{} 的變量。若是你爲了能更新值,建立的是一個指針,那麼須要使用 Elem().Interface() 來獲取變量。但不管是上面的哪一種狀況,你都須要把 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
複製代碼

運行示例

無 make 的建立實例

對於像 slice、map、channel類型,它們須要用 make 建立實例,你也可使用反射實現。slice 使用 reflect.MakeSlice,map 使用 reflect.MakeMap,channel 使用 reflect.MakeChan,你須要提供將建立變量的類型,即 reflect.Type,傳遞給這些函數。成功調用後,你將獲得一個類型爲 reflect.Value 的變量,你能夠經過反射操做這個變量,操做完成後,就 能夠將它轉化爲正常的變量。

func main() {
	// declaring these vars, so I can make a reflect.Type
	intSlice := make([]int, 0)
	mapStringInt := make(map[string]int)

	// here are the reflect.Types
	sliceType := reflect.TypeOf(intSlice)
	mapType := reflect.TypeOf(mapStringInt)

	// and here are the new values that we are making
	intSliceReflect := reflect.MakeSlice(sliceType, 0, 0)
	mapReflect := reflect.MakeMap(mapType)

	// and here we are using them
	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)
}
複製代碼

輸出以下:

[10]
map[hello:10]
複製代碼

運行示例

建立函數

你不只經能夠經過反射建立空間存儲數據,還能夠經過反射提供的函數 reflect.MakeFunc 來建立新的函數。這個函數期待接收參數有兩個,一個是 reflect.Type 類型,而且 Kind 爲 Function,另一個是閉包函數,它的輸入參數類型是 []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)) } 複製代碼

輸出以下:

starting
ending
calling main.timeMe took 1s
starting
ending
calling main.timeMeToo took 2s
4
複製代碼

運行示例

建立一個新的結構

Go 中,反射還能夠在運行時建立一個全新的結構體,你能夠經過傳遞一個 reflect.StructField 的 slice 給 reflect.StructOf 函數來實現。是否是聽起來挺荒誕的,咱們建立的一個新的類型,可是這個類型沒有名字,所以也就沒法將它轉化爲正常的變量。你能夠經過它建立實例,用 Interface() 把它的值轉給類型爲 interface{} 的變量,可是若是要設置它的值,必須來反射來作。

func MakeStruct(vals ...interface{}) interface{} {
	var sfs []reflect.StructField
	for k, v := range vals {
		t := reflect.TypeOf(v)
		sf := reflect.StructField{
			Name: fmt.Sprintf("F%d", (k + 1)),
			Type: t,
		}
		sfs = append(sfs, sf)
	}
	st := reflect.StructOf(sfs)
	so := reflect.New(st)
	return so.Interface()
}

func main() {
	s := MakeStruct(0, "", []int{})
	// this returned a pointer to a struct with 3 fields:
	// an int, a string, and a slice of ints
	// but you can’t actually use any of these fields
	// directly in the code; you have to reflect them
	sr := reflect.ValueOf(s)

	// getting and setting the int field
	fmt.Println(sr.Elem().Field(0).Interface())
	sr.Elem().Field(0).SetInt(20)
	fmt.Println(sr.Elem().Field(0).Interface())

	// getting and setting the string field
	fmt.Println(sr.Elem().Field(1).Interface())
	sr.Elem().Field(1).SetString("reflect me")
	fmt.Println(sr.Elem().Field(1).Interface())

	// getting and setting the []int field
	fmt.Println(sr.Elem().Field(2).Interface())
	v := []int{1, 2, 3}
	rv := reflect.ValueOf(v)
	sr.Elem().Field(2).Set(rv)
	fmt.Println(sr.Elem().Field(2).Interface())
}
複製代碼

輸出以下:

0
20

reflect me
[]
[1 2 3]
複製代碼

運行示例

反射的限制

反射有一個大的限制。雖然運行時能夠經過反射建立新的函數,但沒法用反射建立新的方法,這也就意味着你不能在運行時用反射實現一個接口,用反射建立的結構體使用起來很支離破碎。並且,經過反射建立的結構體,沒法實現 GO 的一個特性 —— 經過匿名字段實現委託模式。

看一個經過結構體實現委託模式的例子,一般狀況下,結構體的字段都會定義名稱。在這例子中,咱們定義了兩個類型,Foo 和 Bar:

type Foo struct {
	A int
}

func (f Foo) Double() int {
	return f.A * 2
}

type Bar struct {
	Foo
	B int
}

type Doubler interface {
	Double() int
}

func DoDouble(d Doubler) {
	fmt.Println(d.Double())
}

func main() {
	f := Foo{10}
	b := Bar{Foo: f, B: 20}
	DoDouble(f) // passed in an instance of Foo; it meets the interface, so no surprise here
    DoDouble(b) // passed in an instance of Bar; it works!
}
複製代碼

運行示例

代碼中顯示,Bar 中的 Foo 字段並無名稱,這使它成了一個匿名或內嵌的字段。Bar 也是知足 Double 接口的,雖然只有 Foo 實現了 Double 方法,這種能力被稱爲委託。在編譯時,Go 會自動爲 Bar 生成 Foo 中的方法。這不是繼承,若是你嘗試給一個只接收 Foo 的函數傳遞 Bar,編譯將不會經過。

若是你用反射去建立一個內嵌字段,而且嘗試去訪問它的方法,將會產生一些很是奇怪的行爲。最好的方式就是,咱們不要用它。關於這個問題,能夠看下 github 的兩個 issue,issue/15924issues/16522。不幸的是,它們尚未任何的進展。

那麼,這會有什麼問題呢?若是支持動態的接口,咱們能夠實現什麼功能?如前面介紹,咱們能經過 Go 的反射建立函數,實現包裹函數,經過 interface 也能夠實現。在 Java 中,這叫作動態代理。當把它和註解結合,將能獲得一個很是強大的能力,實現從命令式編程方式到聲明式編程的切換,一個例子 JDBI,這個 Java 庫讓你能夠在 DAO 層定義一個接口,它的 SQL 查詢經過註解定義。全部數據操做的代碼都是在運行時動態生成,就是如此的強大。

有什麼意義

即便有這個限制,反射依然一個很強大的工具,每位 Go 開發者都應該掌握這項技能。但咱們如何利用好它呢,下一篇英文原版博文再介紹,我將會經過一些庫來探索反射的使用,並將利用它實現一些功能。


波羅學的微信公衆號
相關文章
相關標籤/搜索