作者:Jon Bodner

地址:Learning to Use Go Reflection

什麼是反射

多數情況下,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)。反射的實現源碼位於 Go 標準庫 reflection 包中。

檢查類型

首先,讓我們來看看類型(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(" ", 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(" ", 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(" ", 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(" ", depth+2), "Tag is", f.Tag)
fmt.Println(strings.Repeat(" ", 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
"
, 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
"
, 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/15924 和 issues/16522。不幸的是,它們還沒有任何的進展。

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

有什麼意義

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

推薦閱讀:

相关文章