簡介
反射是一種機制,在編譯時不知道具體類型的情況下,可以透視結(jié)構(gòu)的組成、更新值。使用反射,可以讓我們編寫出能統(tǒng)一處理所有類型的代碼。甚至是編寫這部分代碼時還不存在的類型。一個具體的例子就是fmt.Println()方法,可以打印出我們自定義的結(jié)構(gòu)類型。
雖然,一般來說都不建議在代碼中使用反射。反射影響性能、不易閱讀、將編譯時就能檢查出來的類型問題推遲到運行時以 panic 形式表現(xiàn)出來,這些都是反射的缺點。但是,我認為反射是一定要掌握的,原因如下:
- 很多標準庫和第三方庫都用到了反射,雖然暴露的接口做了封裝,不需要了解反射。但是如果要深入研究這些庫,了解實現(xiàn),閱讀源碼, 反射是繞不過去的。例如
encoding/json,encoding/xml等; - 如果有一個需求,編寫一個可以處理所有類型的函數(shù)或方法,我們就必須會用到反射。因為 Go 的類型數(shù)量是無限的,而且可以自定義類型,所以使用類型斷言是無法達成目標的。
Go 語言標準庫reflect提供了反射功能。
接口
反射是建立在 Go 的類型系統(tǒng)之上的,并且與接口密切相關(guān)。
首先簡單介紹一下接口。Go 語言中的接口約定了一組方法集合,任何定義了這組方法的類型(也稱為實現(xiàn)了接口)的變量都可以賦值給該接口的變量。
package main
import "fmt"
type Animal interface {
Speak()
}
type Cat struct {
}
func (c Cat) Speak() {
fmt.Println("Meow")
}
type Dog struct {
}
func (d Dog) Speak() {
fmt.Println("Bark")
}
func main() {
var a Animal
a = Cat{}
a.Speak()
a = Dog{}
a.Speak()
}
上面代碼中,我們定義了一個Animal接口,它約定了一個方法Speak()。而后定義了兩個結(jié)構(gòu)類型Cat和Dog,都定義了這個方法。這樣,我們就可以將Cat和Dog對象賦值給Animal類型的變量了。
接口變量包含兩部分:類型和值,即(type, value)。類型就是賦值給接口變量的值的類型,值就是賦值給接口變量的值。如果知道接口中存儲的變量類型,我們也可以使用類型斷言通過接口變量獲取具體類型的值:
type Animal interface {
Speak()
}
type Cat struct {
Name string
}
func (c Cat) Speak() {
fmt.Println("Meow")
}
func main() {
var a Animal
a = Cat{Name: "kitty"}
a.Speak()
c := a.(Cat)
fmt.Println(c.Name)
}
上面代碼中,我們知道接口a中保存的是Cat對象,直接使用類型斷言a.(Cat)獲取Cat對象。但是,如果類型斷言的類型與實際存儲的類型不符,會直接 panic。所以實際開發(fā)中,通常使用另一種類型斷言形式c, ok := a.(Cat)。如果類型不符,這種形式不會 panic,而是通過將第二個返回值置為 false 來表明這種情況。
有時候,一個類型定義了很多方法,而不只是接口約定的方法。通過接口,我們只能調(diào)用接口中約定的方法。當然我們也可以將其類型斷言為另一個接口,然后調(diào)用這個接口約定的方法,前提是原對象實現(xiàn)了這個接口:
var r io.Reader
r = new(bytes.Buffer)
w = r.(io.Writer)
io.Reader和io.Writer是標準庫中使用最為頻繁的兩個接口:
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
bytes.Buffer同時實現(xiàn)了這兩個接口,所以byte.Buffer對象可以賦值給io.Reader變量r,然后r可以斷言為io.Writer,因為接口io.Reader中存儲的值也實現(xiàn)了io.Writer接口。
如果一個接口A包含另一個接口B的所有方法,那么接口A的變量可以直接賦值給B的變量,因為A中存儲的值一定實現(xiàn)了A約定的所有方法,那么肯定也實現(xiàn)了B。此時,無須類型斷言。例如標準庫io中還定義了一個io.ReadCloser接口,此接口變量可以直接賦值給io.Reader:
// src/io/io.go
type ReadCloser interface {
Reader
Closer
}
空接口interface{}是比較特殊的一個接口,它沒有約定任何方法。所有類型值都可以賦值給空接口類型的變量,因為它沒有任何方法限制。
有一點特別重要,接口變量之間類型斷言也好,直接賦值也好,其內(nèi)部存儲的(type, value)類型-值對是沒有變化的。只是通過不同的接口能調(diào)用的方法有所不同而已。也是由于這個原因,接口變量中存儲的值一定不是接口類型。
有了這些接口的基礎(chǔ)知識,下面我們介紹反射。
反射基礎(chǔ)
Go 語言中的反射功能由reflect包提供。reflect包定義了一個接口reflect.Type和一個結(jié)構(gòu)體reflect.Value,它們定義了大量的方法用于獲取類型信息,設(shè)置值等。在reflect包內(nèi)部,只有類型描述符實現(xiàn)了reflect.Type接口。由于類型描述符是未導(dǎo)出類型,我們只能通過reflect.TypeOf()方法獲取reflect.Type類型的值:
package main
import (
"fmt"
"reflect"
)
type Cat struct {
Name string
}
func main() {
var f float64 = 3.5
t1 := reflect.TypeOf(f)
fmt.Println(t1.String())
c := Cat{Name: "kitty"}
t2 := reflect.TypeOf(c)
fmt.Println(t2.String())
}
輸出:
float64
main.Cat
Go 語言是靜態(tài)類型的,每個變量在編譯期有且只能有一個確定的、已知的類型,即變量的靜態(tài)類型。靜態(tài)類型在變量聲明的時候就已經(jīng)確定了,無法修改。一個接口變量,它的靜態(tài)類型就是該接口類型。雖然在運行時可以將不同類型的值賦值給它,改變的也只是它內(nèi)部的動態(tài)類型和動態(tài)值。它的靜態(tài)類型始終沒有改變。
reflect.TypeOf()方法就是用來取出接口中的動態(tài)類型部分,以reflect.Type返回。等等!上面代碼好像并沒有接口類型???
我們看下reflect.TypeOf()的定義:
// src/reflect/type.go
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
它接受一個interface{}類型的參數(shù),所以上面的float64和Cat變量會先轉(zhuǎn)為interface{}再傳給方法,reflect.TypeOf()方法獲取的就是這個interface{}中的類型部分。
相應(yīng)地,reflect.ValueOf()方法自然就是獲取接口中的值部分,返回值為reflect.Value類型。在上例基礎(chǔ)上添加下面代碼:
v1 := reflect.ValueOf(f)
fmt.Println(v1)
fmt.Println(v1.String())
v2 := reflect.ValueOf(c)
fmt.Println(v2)
fmt.Println(v2.String())
運行輸出:
3.5
<float64 Value>
{kitty}
<main.Cat Value>
由于fmt.Println()會對reflect.Value類型做特殊處理,打印其內(nèi)部的值,所以上面顯示調(diào)用了reflect.Value.String()方法獲取更多信息。
獲取類型如此常見,fmt提供了格式化符號%T輸出參數(shù)類型:
fmt.Printf("%T\n", 3) // int
Go 語言中類型是無限的,而且可以通過type定義新的類型。但是類型的種類是有限的,reflect包中定義了所有種類的枚舉:
// src/reflect/type.go
type Kind uint
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
一共 26 種,我們可以分類如下:
- 基礎(chǔ)類型
Bool、String以及各種數(shù)值類型(有符號整數(shù)Int/Int8/Int16/Int32/Int64,無符號整數(shù)Uint/Uint8/Uint16/Uint32/Uint64/Uintptr,浮點數(shù)Float32/Float64,復(fù)數(shù)Complex64/Complex128) - 復(fù)合(聚合)類型
Array和Struct - 引用類型
Chan、Func、Ptr、Slice和Map(值類型和引用類型區(qū)分不明顯,這里不引戰(zhàn),大家理解意思就行) - 接口類型
Interface - 非法類型
Invalid,表示它還沒有任何值(reflect.Value的零值就是Invalid類型)
Go 中所有的類型(包括自定義的類型),都是上面這些類型或它們的組合。
例如:
type MyInt int
func main() {
var i int
var j MyInt
i = int(j) // 必須強轉(zhuǎn)
ti := reflect.TypeOf(i)
fmt.Println("type of i:", ti.String())
tj := reflect.TypeOf(j)
fmt.Println("type of j:", tj.String())
fmt.Println("kind of i:", ti.Kind())
fmt.Println("kind of j:", tj.Kind())
}
上面兩個變量的靜態(tài)類型分別為int和MyInt,是不同的。雖然MyInt的底層類型(underlying type)也是int。它們之間的賦值必須要強制類型轉(zhuǎn)換。但是它們的種類是一樣的,都是int。
代碼輸出如下:
type of i: int
type of j: main.MyInt
kind of i: int
kind of j: int
反射用法
由于反射的內(nèi)容和 API 非常多,我們結(jié)合具體用法來介紹。
透視數(shù)據(jù)組成
透視結(jié)構(gòu)體組成,需要以下方法:
-
reflect.ValueOf():獲取反射值對象; -
reflect.Value.NumField():從結(jié)構(gòu)體的反射值對象中獲取它的字段個數(shù); -
reflect.Value.Field(i):從結(jié)構(gòu)體的反射值對象中獲取第i個字段的反射值對象; -
reflect.Kind():從反射值對象中獲取種類; -
reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool():這些方法從反射值對象做取出具體類型。
示例:
type User struct {
Name string
Age int
Married bool
}
func inspectStruct(u interface{}) {
v := reflect.ValueOf(u)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Uint())
case reflect.Bool:
fmt.Printf("field:%d type:%s value:%t\n", i, field.Type().Name(), field.Bool())
case reflect.String:
fmt.Printf("field:%d type:%s value:%q\n", i, field.Type().Name(), field.String())
default:
fmt.Printf("field:%d unhandled kind:%s\n", i, field.Kind())
}
}
}
func main() {
u := User{
Name: "dj",
Age: 18,
Married: true,
}
inspectStruct(u)
}
結(jié)合使用reflect.Value的NumField()和Field()方法可以遍歷結(jié)構(gòu)體的每個字段。然后針對每個字段的Kind做相應(yīng)的處理。
有些方法只有在原對象是某種特定類型時,才能調(diào)用。例如NumField()和Field()方法只有原對象是結(jié)構(gòu)體時才能調(diào)用,否則會panic。
識別出具體類型后,可以調(diào)用反射值對象的對應(yīng)類型方法獲取具體類型的值,例如上面的field.Int()/field.Uint()/field.Bool()/field.String()。但是為了減輕處理的負擔,Int()/Uint()方法對種類做了合并處理,它們只返回相應(yīng)的最大范圍的類型,Int()返回Int64類型,Uint()返回Uint64類型。而Int()/Uint()內(nèi)部會對相應(yīng)的有符號或無符號種類做處理,轉(zhuǎn)為Int64/Uint64返回。下面是reflect.Value.Int()方法的實現(xiàn):
// src/reflect/value.go
func (v Value) Int() int64 {
k := v.kind()
p := v.ptr
switch k {
case Int:
return int64(*(*int)(p))
case Int8:
return int64(*(*int8)(p))
case Int16:
return int64(*(*int16)(p))
case Int32:
return int64(*(*int32)(p))
case Int64:
return *(*int64)(p)
}
panic(&ValueError{"reflect.Value.Int", v.kind()})
}
上面代碼,我們只處理了少部分種類。在實際開發(fā)中,完善的處理需要破費一番功夫,特別是字段是其他復(fù)雜類型,甚至包含循環(huán)引用的時候。
另外,我們也可以透視標準庫中的結(jié)構(gòu)體,并且可以透視其中的未導(dǎo)出字段。使用上面定義的inspectStruct()方法:
inspectStruct(bytes.Buffer{})
bytes.Buffer的結(jié)構(gòu)如下:
type Buffer struct {
buf []byte
off int
lastRead readOp
}
都是未導(dǎo)出的字段,程序輸出:
field:0 unhandled kind:slice
field:1 type:int value:0
field:2 type:readOp value:0
透視map組成,需要以下方法:
-
reflect.Value.MapKeys():將每個鍵的reflect.Value對象組成一個切片返回; -
reflect.Value.MapIndex(k):傳入鍵的reflect.Value對象,返回值的reflect.Value; - 然后可以對鍵和值的
reflect.Value進行和上面一樣的處理。
示例:
func inspectMap(m interface{}) {
v := reflect.ValueOf(m)
for _, k := range v.MapKeys() {
field := v.MapIndex(k)
fmt.Printf("%v => %v\n", k.Interface(), field.Interface())
}
}
func main() {
inspectMap(map[uint32]uint32{
1: 2,
3: 4,
})
}
我這里偷懶了,沒有針對每個Kind去做處理,直接調(diào)用鍵-值reflect.Value的Interface()方法。該方法以空接口的形式返回內(nèi)部包含的值。程序輸出:
1 => 2
3 => 4
同樣地,MapKeys()和MapIndex(k)方法只能在原對象是map類型時才能調(diào)用,否則會panic。
透視切片或數(shù)組組成,需要以下方法:
-
reflect.Value.Len():返回數(shù)組或切片的長度; -
reflect.Value.Index(i):返回第i個元素的reflect.Value值; - 然后對這個
reflect.Value判斷Kind()進行處理。
示例:
func inspectSliceArray(sa interface{}) {
v := reflect.ValueOf(sa)
fmt.Printf("%c", '[')
for i := 0; i < v.Len(); i++ {
elem := v.Index(i)
fmt.Printf("%v ", elem.Interface())
}
fmt.Printf("%c\n", ']')
}
func main() {
inspectSliceArray([]int{1, 2, 3})
inspectSliceArray([3]int{4, 5, 6})
}
程序輸出:
[1 2 3 ]
[4 5 6 ]
同樣地Len()和Index(i)方法只能在原對象是切片,數(shù)組或字符串時才能調(diào)用,其他類型會panic。
透視函數(shù)類型,需要以下方法:
-
reflect.Type.NumIn():獲取函數(shù)參數(shù)個數(shù); -
reflect.Type.In(i):獲取第i個參數(shù)的reflect.Type; -
reflect.Type.NumOut():獲取函數(shù)返回值個數(shù); -
reflect.Type.Out(i):獲取第i個返回值的reflect.Type。
示例:
func Add(a, b int) int {
return a + b
}
func Greeting(name string) string {
return "hello " + name
}
func inspectFunc(name string, f interface{}) {
t := reflect.TypeOf(f)
fmt.Println(name, "input:")
for i := 0; i < t.NumIn(); i++ {
t := t.In(i)
fmt.Print(t.Name())
fmt.Print(" ")
}
fmt.Println()
fmt.Println("output:")
for i := 0; i < t.NumOut(); i++ {
t := t.Out(i)
fmt.Print(t.Name())
fmt.Print(" ")
}
fmt.Println("\n===========")
}
func main() {
inspectFunc("Add", Add)
inspectFunc("Greeting", Greeting)
}
同樣地,只有在原對象是函數(shù)類型的時候才能調(diào)用NumIn()/In()/NumOut()/Out()這些方法,其他類型會panic。
程序輸出:
Add input:
int int
output:
int
===========
Greeting input:
string
output:
string
===========
透視結(jié)構(gòu)體中定義的方法,需要以下方法:
-
reflect.Type.NumMethod():返回結(jié)構(gòu)體定義的方法個數(shù); -
reflect.Type.Method(i):返回第i個方法的reflect.Method對象;
示例:
func inspectMethod(o interface{}) {
t := reflect.TypeOf(o)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(m)
}
}
type User struct {
Name string
Age int
}
func (u *User) SetName(n string) {
u.Name = n
}
func (u *User) SetAge(a int) {
u.Age = a
}
func main() {
u := User{
Name: "dj",
Age: 18,
}
inspectMethod(&u)
}
reflect.Method定義如下:
// src/reflect/type.go
type Method struct {
Name string // 方法名
PkgPath string
Type Type // 方法類型(即函數(shù)類型)
Func Value // 方法值(以接收器作為第一個參數(shù))
Index int // 是結(jié)構(gòu)體中的第幾個方法
}
事實上,reflect.Value也定義了NumMethod()/Method(i)這些方法。區(qū)別在于:reflect.Type.Method(i)返回的是一個reflect.Method對象,可以獲取方法名、類型、是結(jié)構(gòu)體中的第幾個方法等信息。如果要通過這個reflect.Method調(diào)用方法,必須使用Func字段,而且要傳入接收器的reflect.Value作為第一個參數(shù):
m.Func.Call(v, ...args)
但是reflect.Value.Method(i)返回一個reflect.Value對象,它總是以調(diào)用Method(i)方法的reflect.Value作為接收器對象,不需要額外傳入。而且直接使用Call()發(fā)起方法調(diào)用:
m.Call(...args)
reflect.Type和reflect.Value有不少同名方法,使用時需要注意甄別。
調(diào)用函數(shù)或方法
調(diào)用函數(shù),需要以下方法:
-
reflect.Value.Call():使用reflect.ValueOf()生成每個參數(shù)的反射值對象,然后組成切片傳給Call()方法。Call()方法執(zhí)行函數(shù)調(diào)用,返回[]reflect.Value。其中每個元素都是原返回值的反射值對象。
示例:
func Add(a, b int) int {
return a + b
}
func Greeting(name string) string {
return "hello " + name
}
func invoke(f interface{}, args ...interface{}) {
v := reflect.ValueOf(f)
argsV := make([]reflect.Value, 0, len(args))
for _, arg := range args {
argsV = append(argsV, reflect.ValueOf(arg))
}
rets := v.Call(argsV)
fmt.Println("ret:")
for _, ret := range rets {
fmt.Println(ret.Interface())
}
}
func main() {
invoke(Add, 1, 2)
invoke(Greeting, "dj")
}
我們封裝一個invoke()方法,以interface{}空接口接收函數(shù)對象,以interface{}可變參數(shù)接收函數(shù)調(diào)用的參數(shù)。函數(shù)內(nèi)部首先調(diào)用reflect.ValueOf()方法獲得函數(shù)對象的反射值對象。然后依次對每個參數(shù)調(diào)用reflect.ValueOf(),生成參數(shù)的反射值對象切片。最后調(diào)用函數(shù)反射值對象的Call()方法,輸出返回值。
程序運行結(jié)果:
ret:
3
ret:
hello dj
方法的調(diào)用也是類似的:
type M struct {
a, b int
op rune
}
func (m M) Op() int {
switch m.op {
case '+':
return m.a + m.b
case '-':
return m.a - m.b
case '*':
return m.a * m.b
case '/':
return m.a / m.b
default:
panic("invalid op")
}
}
func main() {
m1 := M{1, 2, '+'}
m2 := M{3, 4, '-'}
m3 := M{5, 6, '*'}
m4 := M{8, 2, '/'}
invoke(m1.Op)
invoke(m2.Op)
invoke(m3.Op)
invoke(m4.Op)
}
運行結(jié)果:
ret:
3
ret:
-1
ret:
30
ret:
4
以上是在編譯期明確知道方法名的情況下發(fā)起調(diào)用。如果只給一個結(jié)構(gòu)體對象,通過參數(shù)指定具體調(diào)用哪個方法該怎么做呢?這需要以下方法:
-
reflect.Value.MethodByName(name):獲取結(jié)構(gòu)體中定義的名為name的方法的reflect.Value對象,這個方法默認有接收器參數(shù),即調(diào)用MethodByName()方法的reflect.Value。
示例:
type Math struct {
a, b int
}
func (m Math) Add() int {
return m.a + m.b
}
func (m Math) Sub() int {
return m.a - m.b
}
func (m Math) Mul() int {
return m.a * m.b
}
func (m Math) Div() int {
return m.a / m.b
}
func invokeMethod(obj interface{}, name string, args ...interface{}) {
v := reflect.ValueOf(obj)
m := v.MethodByName(name)
argsV := make([]reflect.Value, 0, len(args))
for _, arg := range args {
argsV = append(argsV, reflect.ValueOf(arg))
}
rets := m.Call(argsV)
fmt.Println("ret:")
for _, ret := range rets {
fmt.Println(ret.Interface())
}
}
func main() {
m := Math{a: 10, b: 2}
invokeMethod(m, "Add")
invokeMethod(m, "Sub")
invokeMethod(m, "Mul")
invokeMethod(m, "Div")
}
我們可以在結(jié)構(gòu)體的反射值對象上使用NumMethod()和Method()遍歷它定義的所有方法。
實戰(zhàn)案例
使用前面介紹的方法,我們很容易實現(xiàn)一個簡單的、基于 HTTP 的 RPC 調(diào)用。約定格式:路徑名/obj/method/arg1/arg2調(diào)用obj.method(arg1, arg2)方法。
首先定義兩個結(jié)構(gòu)體,并為它們定義方法,我們約定可導(dǎo)出的方法會注冊為 RPC 方法。并且方法必須返回兩個值:一個結(jié)果,一個錯誤。
type StringObject struct{}
func (StringObject) Concat(s1, s2 string) (string, error) {
return s1 + s2, nil
}
func (StringObject) ToUpper(s string) (string, error) {
return strings.ToUpper(s), nil
}
func (StringObject) ToLower(s string) (string, error) {
return strings.ToLower(s), nil
}
type MathObject struct{}
func (MathObject) Add(a, b int) (int, error) {
return a + b, nil
}
func (MathObject) Sub(a, b int) (int, error) {
return a - b, nil
}
func (MathObject) Mul(a, b int) (int, error) {
return a * b, nil
}
func (MathObject) Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divided by zero")
}
return a / b, nil
}
接下來我們定義一個結(jié)構(gòu)表示可以調(diào)用的 RPC 方法:
type RpcMethod struct {
method reflect.Value
args []reflect.Type
}
其中method是方法的反射值對象,args是各個參數(shù)的類型。我們定義一個函數(shù)從對象中提取可以 RPC 調(diào)用的方法:
var (
mapObjMethods map[string]map[string]RpcMethod
)
func init() {
mapObjMethods = make(map[string]map[string]RpcMethod)
}
func registerMethods(objName string, o interface{}) {
v := reflect.ValueOf(o)
mapObjMethods[objName] = make(map[string]RpcMethod)
for i := 0; i < v.NumMethod(); i++ {
m := v.Method(i)
if m.Type().NumOut() != 2 {
// 排除不是兩個返回值的
continue
}
if m.Type().Out(1).Name() != "error" {
// 排除第二個返回值不是 error 的
continue
}
t := v.Type().Method(i)
methodName := t.Name
if len(methodName) <= 1 || strings.ToUpper(methodName[0:1]) != methodName[0:1] {
// 排除非導(dǎo)出方法
continue
}
types := make([]reflect.Type, 0, 1)
for j := 0; j < m.Type().NumIn(); j++ {
types = append(types, m.Type().In(j))
}
mapObjMethods[objName][methodName] = RpcMethod{
m, types,
}
}
}
registerMethods()函數(shù)使用reflect.Value.NumMethod()和reflect.Method(i)從對象中遍歷方法,排除掉不是兩個返回值的、第二個返回值不是 error 的或者非導(dǎo)出的方法。
然后定義一個 http 處理器:
func handler(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path[1:], "/")
if len(parts) < 2 {
handleError(w, errors.New("invalid request"))
return
}
m := lookupMethod(parts[0], parts[1])
if m.method.IsZero() {
handleError(w, fmt.Errorf("no such method:%s in object:%s", parts[0], parts[1]))
return
}
argSs := parts[2:]
if len(m.args) != len(argSs) {
handleError(w, errors.New("inconsistant args num"))
return
}
argVs := make([]reflect.Value, 0, 1)
for i, t := range m.args {
switch t.Kind() {
case reflect.Int:
value, _ := strconv.Atoi(argSs[i])
argVs = append(argVs, reflect.ValueOf(value))
case reflect.String:
argVs = append(argVs, reflect.ValueOf(argSs[i]))
default:
handleError(w, fmt.Errorf("invalid arg type:%s", t.Kind()))
return
}
}
ret := m.method.Call(argVs)
err := ret[1].Interface()
if err != nil {
handleError(w, err.(error))
return
}
response(w, ret[0].Interface())
}
我們將路徑分割得到一個切片,第一個元素為對象名(即math或string),第二個元素為方法名(即Add/Sub/Mul/Div等),后面的都是參數(shù)。接著,我們查找要調(diào)用的方法,根據(jù)注冊時記錄的各個參數(shù)的類型將路徑中的字符串轉(zhuǎn)換為對應(yīng)類型。然后調(diào)用,檢查第二個返回值是否為nil可以獲知方法調(diào)用是否出錯。成功調(diào)用則返回結(jié)果。
最后我們只需要啟動一個 http 服務(wù)器即可:
func main() {
registerMethods("math", MathObject{})
registerMethods("string", StringObject{})
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
完整代碼在 Github 倉庫中。運行:
$ go run main.go
使用 curl 來驗證:
$ curl localhost:8080/math/Add/1/2
{"data":3}
$ curl localhost:8080/math/Sub/10/2
{"data":8}
$ curl localhost:8080/math/Div/10/2
{"data":5}
$ curl localhost:8080/math/Div/10/0
{"error":"divided by zero"}
$ curl localhost:8080/string/Concat/abc/def
{"data":"abcdef"}
當然,這只是一個簡單的實現(xiàn),還有很多錯誤處理沒有考慮,方法參數(shù)的類型目前只支持int和string,感興趣可以去完善一下。
設(shè)置值
首先介紹一個概念:可尋址??蓪ぶ肥强梢酝ㄟ^反射獲得其地址的能力??蓪ぶ放c指針緊密相關(guān)。所有通過reflect.ValueOf()得到的reflect.Value都不可尋址。因為它們只保存了自身的值,對自身的地址一無所知。例如指針p *int保存了另一個int數(shù)據(jù)在內(nèi)存中的地址,但是它自身的地址無法通過自身獲取到,因為在將它傳給reflect.ValueOf()時,其自身地址信息就丟失了。我們可以通過reflect.Value.CanAddr()判斷是否可尋址:
func main() {
x := 2
a := reflect.ValueOf(2)
b := reflect.ValueOf(x)
c := reflect.ValueOf(&x)
fmt.Println(a.CanAddr()) // false
fmt.Println(b.CanAddr()) // false
fmt.Println(c.CanAddr()) // false
}
雖然指針不可尋址,但是我們可以在其反射對象上調(diào)用Elem()獲取它指向的元素的reflect.Value。這個reflect.Value就可以尋址了,因為是通過reflect.Value.Elem()獲取的值,可以記錄這個獲取路徑。因而得到的reflect.Value中保存了它的地址:
d := c.Elem()
fmt.Println(d.CanAddr())
另外通過切片反射對象的Index(i)方法得到的reflect.Value也是可尋址的,我們總是可以通過切片得到某個索引的地址。通過結(jié)構(gòu)體的指針獲取到的字段也是可尋址的:
type User struct {
Name string
Age int
}
s := []int{1, 2, 3}
sv := reflect.ValueOf(s)
e := sv.Index(1)
fmt.Println(e.CanAddr()) // true
u := &User{Name: "dj", Age: 18}
uv := reflect.ValueOf(u)
f := uv.Elem().Field(0)
fmt.Println(f.CanAddr()) // true
如果一個reflect.Value可尋址,我們可以調(diào)用其Addr()方法返回一個reflect.Value,包含一個指向原始數(shù)據(jù)的指針。然后在這個reflect.Value上調(diào)用Interface{}方法,會返回一個包含這個指針的interface{}值。如果我們知道類型,可以使用類型斷言將其轉(zhuǎn)為一個普通指針。通過普通指針來更新值:
func main() {
x := 2
d := reflect.ValueOf(&x).Elem()
px := d.Addr().Interface().(*int)
*px = 3
fmt.Println(x) // 3
}
這樣的更新方法有點麻煩,我們可以直接通過可尋址的reflect.Value調(diào)用Set()方法來更新,不用通過指針:
d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4
如果傳入的類型不匹配,會 panic。reflect.Value為基本類型提供特殊的Set方法:SetInt、SetUint、SetFloat等:
d.SetInt(5)
fmt.Println(x) // 5
反射可以讀取未導(dǎo)出結(jié)構(gòu)字段的值,但是不能更新這些值。一個可尋址的reflect.Value會記錄它是否是通過遍歷一個未導(dǎo)出字段來獲得的,如果是則不允許修改。所以在更新前使用CanAddr()判斷并不保險。CanSet()可以正確判斷一個值是否可以修改。
CanSet()判斷的是可設(shè)置性,它是比可尋址性更嚴格的性質(zhì)。如果一個reflect.Value是可設(shè)置的,它一定是可尋址的。反之則不然:
type User struct {
Name string
age int
}
u := &User{Name: "dj", age: 18}
uv := reflect.ValueOf(u)
name := uv.Elem().Field(0)
fmt.Println(name.CanAddr(), name.CanSet()) // true true
age := uv.Elem().Field(1)
fmt.Println(age.CanAddr(), age.CanSet()) // true false
name.SetString("lidajun")
fmt.Println(u) // &{lidajun 18}
// 報錯
// age.SetInt(20)
StructTag
在定義結(jié)構(gòu)體時,可以為每個字段指定一個標簽,我們可以使用反射讀取這些標簽:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := &User{Name: "dj", Age: 18}
t := reflect.TypeOf(u).Elem()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Println(f.Tag)
}
}
標簽就是一個普通的字符串,上面程序輸出:
json:"name"
json:"age"
StructTag定義在reflect/type.go文件中:
type StructTag string
一般慣例是將各個鍵值對,使用空格分開,鍵值之間使用:。例如:
`json:"name" xml:"age"`
StructTag提供Get()方法獲取鍵對應(yīng)的值。
總結(jié)
本文系統(tǒng)地介紹了 Go 語言中的反射機制,從類型、接口到反射用法。還使用反射實現(xiàn)了一個簡單的基于 HTTP 的 RPC 庫。反射雖然在平時開發(fā)中不建議使用,但是閱讀源碼,自己編寫庫的時候需要頻繁用到反射知識。熟練掌握反射可以使源碼閱讀事半功倍。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
- Rob Pike, laws of reflection: https://golang.org/doc/articles/laws_of_reflection.html
- Go 程序設(shè)計語言,第 12 章:反射
- reflect 官方文檔,https://pkg.go.dev/reflect
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
歡迎關(guān)注我的微信公眾號【GoUpUp】,共同學習,一起進步~