Go 每日一庫(kù)之 reflect

簡(jiǎn)介

反射是一種機(jī)制徒欣,在編譯時(shí)不知道具體類型的情況下攒磨,可以透視結(jié)構(gòu)的組成泳桦、更新值。使用反射娩缰,可以讓我們編寫(xiě)出能統(tǒng)一處理所有類型的代碼灸撰。甚至是編寫(xiě)這部分代碼時(shí)還不存在的類型。一個(gè)具體的例子就是fmt.Println()方法拼坎,可以打印出我們自定義的結(jié)構(gòu)類型浮毯。

雖然,一般來(lái)說(shuō)都不建議在代碼中使用反射泰鸡。反射影響性能债蓝、不易閱讀、將編譯時(shí)就能檢查出來(lái)的類型問(wèn)題推遲到運(yùn)行時(shí)以 panic 形式表現(xiàn)出來(lái)盛龄,這些都是反射的缺點(diǎn)饰迹。但是芳誓,我認(rèn)為反射是一定要掌握的,原因如下:

  • 很多標(biāo)準(zhǔn)庫(kù)和第三方庫(kù)都用到了反射啊鸭,雖然暴露的接口做了封裝锹淌,不需要了解反射。但是如果要深入研究這些庫(kù)赠制,了解實(shí)現(xiàn)赂摆,閱讀源碼, 反射是繞不過(guò)去的钟些。例如encoding/json库正,encoding/xml等;
  • 如果有一個(gè)需求厘唾,編寫(xiě)一個(gè)可以處理所有類型的函數(shù)或方法褥符,我們就必須會(huì)用到反射。因?yàn)?Go 的類型數(shù)量是無(wú)限的抚垃,而且可以自定義類型喷楣,所以使用類型斷言是無(wú)法達(dá)成目標(biāo)的。

Go 語(yǔ)言標(biāo)準(zhǔn)庫(kù)reflect提供了反射功能鹤树。

接口

反射是建立在 Go 的類型系統(tǒng)之上的铣焊,并且與接口密切相關(guān)。

首先簡(jiǎn)單介紹一下接口罕伯。Go 語(yǔ)言中的接口約定了一組方法集合曲伊,任何定義了這組方法的類型(也稱為實(shí)現(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()
}

上面代碼中追他,我們定義了一個(gè)Animal接口坟募,它約定了一個(gè)方法Speak()。而后定義了兩個(gè)結(jié)構(gòu)類型CatDog邑狸,都定義了這個(gè)方法懈糯。這樣,我們就可以將CatDog對(duì)象賦值給Animal類型的變量了单雾。

接口變量包含兩部分:類型和值赚哗,即(type, value)。類型就是賦值給接口變量的值的類型硅堆,值就是賦值給接口變量的值屿储。如果知道接口中存儲(chǔ)的變量類型,我們也可以使用類型斷言通過(guò)接口變量獲取具體類型的值:

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對(duì)象够掠,直接使用類型斷言a.(Cat)獲取Cat對(duì)象。但是朴乖,如果類型斷言的類型與實(shí)際存儲(chǔ)的類型不符祖屏,會(huì)直接 panic。所以實(shí)際開(kāi)發(fā)中买羞,通常使用另一種類型斷言形式c, ok := a.(Cat)袁勺。如果類型不符,這種形式不會(huì) panic畜普,而是通過(guò)將第二個(gè)返回值置為 false 來(lái)表明這種情況期丰。

有時(shí)候,一個(gè)類型定義了很多方法吃挑,而不只是接口約定的方法钝荡。通過(guò)接口,我們只能調(diào)用接口中約定的方法舶衬。當(dāng)然我們也可以將其類型斷言為另一個(gè)接口,然后調(diào)用這個(gè)接口約定的方法,前提是原對(duì)象實(shí)現(xiàn)了這個(gè)接口:

var r io.Reader
r = new(bytes.Buffer)
w = r.(io.Writer)

io.Readerio.Writer是標(biāo)準(zhǔn)庫(kù)中使用最為頻繁的兩個(gè)接口:

// 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同時(shí)實(shí)現(xiàn)了這兩個(gè)接口态贤,所以byte.Buffer對(duì)象可以賦值給io.Reader變量r得哆,然后r可以斷言為io.Writer,因?yàn)榻涌?code>io.Reader中存儲(chǔ)的值也實(shí)現(xiàn)了io.Writer接口虽画。

如果一個(gè)接口A包含另一個(gè)接口B的所有方法舞蔽,那么接口A的變量可以直接賦值給B的變量,因?yàn)?code>A中存儲(chǔ)的值一定實(shí)現(xiàn)了A約定的所有方法码撰,那么肯定也實(shí)現(xiàn)了B渗柿。此時(shí),無(wú)須類型斷言脖岛。例如標(biāo)準(zhǔn)庫(kù)io中還定義了一個(gè)io.ReadCloser接口朵栖,此接口變量可以直接賦值給io.Reader

// src/io/io.go
type ReadCloser interface {
  Reader
  Closer
}

空接口interface{}是比較特殊的一個(gè)接口,它沒(méi)有約定任何方法柴梆。所有類型值都可以賦值給空接口類型的變量混槐,因?yàn)樗鼪](méi)有任何方法限制

有一點(diǎn)特別重要轩性,接口變量之間類型斷言也好声登,直接賦值也好,其內(nèi)部存儲(chǔ)的(type, value)類型-值對(duì)是沒(méi)有變化的揣苏。只是通過(guò)不同的接口能調(diào)用的方法有所不同而已悯嗓。也是由于這個(gè)原因,接口變量中存儲(chǔ)的值一定不是接口類型卸察。

有了這些接口的基礎(chǔ)知識(shí)脯厨,下面我們介紹反射。

反射基礎(chǔ)

Go 語(yǔ)言中的反射功能由reflect包提供坑质。reflect包定義了一個(gè)接口reflect.Type和一個(gè)結(jié)構(gòu)體reflect.Value合武,它們定義了大量的方法用于獲取類型信息临梗,設(shè)置值等。在reflect包內(nèi)部稼跳,只有類型描述符實(shí)現(xiàn)了reflect.Type接口盟庞。由于類型描述符是未導(dǎo)出類型,我們只能通過(guò)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 語(yǔ)言是靜態(tài)類型的汤善,每個(gè)變量在編譯期有且只能有一個(gè)確定的什猖、已知的類型,即變量的靜態(tài)類型红淡。靜態(tài)類型在變量聲明的時(shí)候就已經(jīng)確定了不狮,無(wú)法修改。一個(gè)接口變量在旱,它的靜態(tài)類型就是該接口類型摇零。雖然在運(yùn)行時(shí)可以將不同類型的值賦值給它,改變的也只是它內(nèi)部的動(dòng)態(tài)類型和動(dòng)態(tài)值桶蝎。它的靜態(tài)類型始終沒(méi)有改變遂黍。

reflect.TypeOf()方法就是用來(lái)取出接口中的動(dòng)態(tài)類型部分,以reflect.Type返回俊嗽。等等雾家!上面代碼好像并沒(méi)有接口類型啊绍豁?

我們看下reflect.TypeOf()的定義:

// src/reflect/type.go
func TypeOf(i interface{}) Type {
  eface := *(*emptyInterface)(unsafe.Pointer(&i))
  return toType(eface.typ)
}

它接受一個(gè)interface{}類型的參數(shù)芯咧,所以上面的float64Cat變量會(huì)先轉(zhuǎn)為interface{}再傳給方法,reflect.TypeOf()方法獲取的就是這個(gè)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())

運(yùn)行輸出:

3.5
<float64 Value>
{kitty}
<main.Cat Value>

由于fmt.Println()會(huì)對(duì)reflect.Value類型做特殊處理无拗,打印其內(nèi)部的值,所以上面顯示調(diào)用了reflect.Value.String()方法獲取更多信息昧碉。

獲取類型如此常見(jiàn)英染,fmt提供了格式化符號(hào)%T輸出參數(shù)類型:

fmt.Printf("%T\n", 3) // int

Go 語(yǔ)言中類型是無(wú)限的,而且可以通過(guò)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ù)值類型(有符號(hào)整數(shù)Int/Int8/Int16/Int32/Int64闪金,無(wú)符號(hào)整數(shù)Uint/Uint8/Uint16/Uint32/Uint64/Uintptr,浮點(diǎn)數(shù)Float32/Float64,復(fù)數(shù)Complex64/Complex128
  • 復(fù)合(聚合)類型ArrayStruct
  • 引用類型Chan哎垦、Func囱嫩、PtrSliceMap(值類型和引用類型區(qū)分不明顯漏设,這里不引戰(zhàn)墨闲,大家理解意思就行)
  • 接口類型Interface
  • 非法類型Invalid,表示它還沒(méi)有任何值(reflect.Value的零值就是Invalid類型)

Go 中所有的類型(包括自定義的類型)愿题,都是上面這些類型或它們的組合损俭。

例如:

type MyInt int

func main() {
  var i int
  var j MyInt

  i = int(j) // 必須強(qiáng)轉(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())
}

上面兩個(gè)變量的靜態(tài)類型分別為intMyInt蛙奖,是不同的潘酗。雖然MyInt的底層類型(underlying type)也是int。它們之間的賦值必須要強(qiáng)制類型轉(zhuǎn)換雁仲。但是它們的種類是一樣的仔夺,都是int

代碼輸出如下:

type of i: int
type of j: main.MyInt
kind of i: int
kind of j: int

反射用法

由于反射的內(nèi)容和 API 非常多攒砖,我們結(jié)合具體用法來(lái)介紹缸兔。

透視數(shù)據(jù)組成

透視結(jié)構(gòu)體組成,需要以下方法:

  • reflect.ValueOf():獲取反射值對(duì)象吹艇;
  • reflect.Value.NumField():從結(jié)構(gòu)體的反射值對(duì)象中獲取它的字段個(gè)數(shù)惰蜜;
  • reflect.Value.Field(i):從結(jié)構(gòu)體的反射值對(duì)象中獲取第i個(gè)字段的反射值對(duì)象;
  • reflect.Kind():從反射值對(duì)象中獲取種類受神;
  • reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool():這些方法從反射值對(duì)象做取出具體類型抛猖。

示例:

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.ValueNumField()Field()方法可以遍歷結(jié)構(gòu)體的每個(gè)字段。然后針對(duì)每個(gè)字段的Kind做相應(yīng)的處理鼻听。

有些方法只有在原對(duì)象是某種特定類型時(shí)财著,才能調(diào)用。例如NumField()Field()方法只有原對(duì)象是結(jié)構(gòu)體時(shí)才能調(diào)用撑碴,否則會(huì)panic撑教。

識(shí)別出具體類型后,可以調(diào)用反射值對(duì)象的對(duì)應(yīng)類型方法獲取具體類型的值醉拓,例如上面的field.Int()/field.Uint()/field.Bool()/field.String()伟姐。但是為了減輕處理的負(fù)擔(dān),Int()/Uint()方法對(duì)種類做了合并處理亿卤,它們只返回相應(yīng)的最大范圍的類型玫镐,Int()返回Int64類型,Uint()返回Uint64類型怠噪。而Int()/Uint()內(nèi)部會(huì)對(duì)相應(yīng)的有符號(hào)或無(wú)符號(hào)種類做處理恐似,轉(zhuǎn)為Int64/Uint64返回。下面是reflect.Value.Int()方法的實(shí)現(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()})
}

上面代碼傍念,我們只處理了少部分種類矫夷。在實(shí)際開(kāi)發(fā)中葛闷,完善的處理需要破費(fèi)一番功夫,特別是字段是其他復(fù)雜類型双藕,甚至包含循環(huán)引用的時(shí)候淑趾。

另外,我們也可以透視標(biāo)準(zhǔn)庫(kù)中的結(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():將每個(gè)鍵的reflect.Value對(duì)象組成一個(gè)切片返回延蟹;
  • reflect.Value.MapIndex(k):傳入鍵的reflect.Value對(duì)象,返回值的reflect.Value叶堆;
  • 然后可以對(duì)鍵和值的reflect.Value進(jìn)行和上面一樣的處理阱飘。

示例:

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,
  })
}

我這里偷懶了,沒(méi)有針對(duì)每個(gè)Kind去做處理虱颗,直接調(diào)用鍵-值reflect.ValueInterface()方法沥匈。該方法以空接口的形式返回內(nèi)部包含的值。程序輸出:

1 => 2
3 => 4

同樣地忘渔,MapKeys()MapIndex(k)方法只能在原對(duì)象是map類型時(shí)才能調(diào)用高帖,否則會(huì)panic

透視切片或數(shù)組組成畦粮,需要以下方法:

  • reflect.Value.Len():返回?cái)?shù)組或切片的長(zhǎng)度散址;
  • reflect.Value.Index(i):返回第i個(gè)元素的reflect.Value值;
  • 然后對(duì)這個(gè)reflect.Value判斷Kind()進(jìn)行處理锈玉。

示例:

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)方法只能在原對(duì)象是切片爪飘,數(shù)組或字符串時(shí)才能調(diào)用,其他類型會(huì)panic拉背。

透視函數(shù)類型师崎,需要以下方法:

  • reflect.Type.NumIn():獲取函數(shù)參數(shù)個(gè)數(shù);
  • reflect.Type.In(i):獲取第i個(gè)參數(shù)的reflect.Type椅棺;
  • reflect.Type.NumOut():獲取函數(shù)返回值個(gè)數(shù)犁罩;
  • reflect.Type.Out(i):獲取第i個(gè)返回值的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)
}

同樣地两疚,只有在原對(duì)象是函數(shù)類型的時(shí)候才能調(diào)用NumIn()/In()/NumOut()/Out()這些方法床估,其他類型會(huì)panic

程序輸出:

Add input:
int int
output:
int
===========
Greeting input:
string
output:
string
===========

透視結(jié)構(gòu)體中定義的方法诱渤,需要以下方法:

  • reflect.Type.NumMethod():返回結(jié)構(gòu)體定義的方法個(gè)數(shù)丐巫;
  • reflect.Type.Method(i):返回第i個(gè)方法的reflect.Method對(duì)象;

示例:

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 // 方法值(以接收器作為第一個(gè)參數(shù))
  Index int   // 是結(jié)構(gòu)體中的第幾個(gè)方法
}

事實(shí)上,reflect.Value也定義了NumMethod()/Method(i)這些方法递胧。區(qū)別在于:reflect.Type.Method(i)返回的是一個(gè)reflect.Method對(duì)象碑韵,可以獲取方法名、類型缎脾、是結(jié)構(gòu)體中的第幾個(gè)方法等信息祝闻。如果要通過(guò)這個(gè)reflect.Method調(diào)用方法,必須使用Func字段遗菠,而且要傳入接收器的reflect.Value作為第一個(gè)參數(shù):

m.Func.Call(v, ...args)

但是reflect.Value.Method(i)返回一個(gè)reflect.Value對(duì)象联喘,它總是以調(diào)用Method(i)方法的reflect.Value作為接收器對(duì)象,不需要額外傳入辙纬。而且直接使用Call()發(fā)起方法調(diào)用:

m.Call(...args)

reflect.Typereflect.Value有不少同名方法豁遭,使用時(shí)需要注意甄別。

調(diào)用函數(shù)或方法

調(diào)用函數(shù)牲平,需要以下方法:

  • reflect.Value.Call():使用reflect.ValueOf()生成每個(gè)參數(shù)的反射值對(duì)象堤框,然后組成切片傳給Call()方法域滥。Call()方法執(zhí)行函數(shù)調(diào)用纵柿,返回[]reflect.Value。其中每個(gè)元素都是原返回值的反射值對(duì)象启绰。

示例:

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")
}

我們封裝一個(gè)invoke()方法昂儒,以interface{}空接口接收函數(shù)對(duì)象,以interface{}可變參數(shù)接收函數(shù)調(diào)用的參數(shù)委可。函數(shù)內(nèi)部首先調(diào)用reflect.ValueOf()方法獲得函數(shù)對(duì)象的反射值對(duì)象渊跋。然后依次對(duì)每個(gè)參數(shù)調(diào)用reflect.ValueOf(),生成參數(shù)的反射值對(duì)象切片着倾。最后調(diào)用函數(shù)反射值對(duì)象的Call()方法拾酝,輸出返回值。

程序運(yùn)行結(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)
}

運(yùn)行結(jié)果:

ret:
3
ret:
-1
ret:
30
ret:
4

以上是在編譯期明確知道方法名的情況下發(fā)起調(diào)用卡者。如果只給一個(gè)結(jié)構(gòu)體對(duì)象蒿囤,通過(guò)參數(shù)指定具體調(diào)用哪個(gè)方法該怎么做呢?這需要以下方法:

  • reflect.Value.MethodByName(name):獲取結(jié)構(gòu)體中定義的名為name的方法的reflect.Value對(duì)象崇决,這個(gè)方法默認(rèn)有接收器參數(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)體的反射值對(duì)象上使用NumMethod()Method()遍歷它定義的所有方法恒傻。

實(shí)戰(zhàn)案例

使用前面介紹的方法脸侥,我們很容易實(shí)現(xiàn)一個(gè)簡(jiǎn)單的、基于 HTTP 的 RPC 調(diào)用盈厘。約定格式:路徑名/obj/method/arg1/arg2調(diào)用obj.method(arg1, arg2)方法睁枕。

首先定義兩個(gè)結(jié)構(gòu)體,并為它們定義方法,我們約定可導(dǎo)出的方法會(huì)注冊(cè)為 RPC 方法外遇。并且方法必須返回兩個(gè)值:一個(gè)結(jié)果拒逮,一個(gè)錯(cuò)誤。

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
}

接下來(lái)我們定義一個(gè)結(jié)構(gòu)表示可以調(diào)用的 RPC 方法:

type RpcMethod struct {
  method reflect.Value
  args   []reflect.Type
}

其中method是方法的反射值對(duì)象臀规,args是各個(gè)參數(shù)的類型滩援。我們定義一個(gè)函數(shù)從對(duì)象中提取可以 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 {
      // 排除不是兩個(gè)返回值的
      continue
    }

    if m.Type().Out(1).Name() != "error" {
      // 排除第二個(gè)返回值不是 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)從對(duì)象中遍歷方法,排除掉不是兩個(gè)返回值的塔嬉、第二個(gè)返回值不是 error 的或者非導(dǎo)出的方法玩徊。

然后定義一個(gè) 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())
}

我們將路徑分割得到一個(gè)切片,第一個(gè)元素為對(duì)象名(即mathstring)谨究,第二個(gè)元素為方法名(即Add/Sub/Mul/Div等)恩袱,后面的都是參數(shù)。接著胶哲,我們查找要調(diào)用的方法畔塔,根據(jù)注冊(cè)時(shí)記錄的各個(gè)參數(shù)的類型將路徑中的字符串轉(zhuǎn)換為對(duì)應(yīng)類型。然后調(diào)用鸯屿,檢查第二個(gè)返回值是否為nil可以獲知方法調(diào)用是否出錯(cuò)澈吨。成功調(diào)用則返回結(jié)果。

最后我們只需要啟動(dòng)一個(gè) 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 倉(cāng)庫(kù)中寄摆。運(yùn)行:

$ go run main.go

使用 curl 來(lái)驗(yàn)證:

$ 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"}

當(dāng)然谅辣,這只是一個(gè)簡(jiǎn)單的實(shí)現(xiàn),還有很多錯(cuò)誤處理沒(méi)有考慮婶恼,方法參數(shù)的類型目前只支持intstring桑阶,感興趣可以去完善一下。

設(shè)置值

首先介紹一個(gè)概念:可尋址勾邦◎悸迹可尋址是可以通過(guò)反射獲得其地址的能力【炱可尋址與指針緊密相關(guān)萎河。所有通過(guò)reflect.ValueOf()得到的reflect.Value都不可尋址。因?yàn)樗鼈冎槐4媪俗陨淼闹登撸瑢?duì)自身的地址一無(wú)所知公壤。例如指針p *int保存了另一個(gè)int數(shù)據(jù)在內(nèi)存中的地址,但是它自身的地址無(wú)法通過(guò)自身獲取到椎椰,因?yàn)樵趯⑺鼈鹘oreflect.ValueOf()時(shí)厦幅,其自身地址信息就丟失了。我們可以通過(guò)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
}

雖然指針不可尋址慨飘,但是我們可以在其反射對(duì)象上調(diào)用Elem()獲取它指向的元素的reflect.Value确憨。這個(gè)reflect.Value就可以尋址了译荞,因?yàn)槭峭ㄟ^(guò)reflect.Value.Elem()獲取的值,可以記錄這個(gè)獲取路徑休弃。因而得到的reflect.Value中保存了它的地址:

d := c.Elem()
fmt.Println(d.CanAddr())

另外通過(guò)切片反射對(duì)象的Index(i)方法得到的reflect.Value也是可尋址的吞歼,我們總是可以通過(guò)切片得到某個(gè)索引的地址。通過(guò)結(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

如果一個(gè)reflect.Value可尋址塔猾,我們可以調(diào)用其Addr()方法返回一個(gè)reflect.Value篙骡,包含一個(gè)指向原始數(shù)據(jù)的指針。然后在這個(gè)reflect.Value上調(diào)用Interface{}方法丈甸,會(huì)返回一個(gè)包含這個(gè)指針的interface{}值糯俗。如果我們知道類型,可以使用類型斷言將其轉(zhuǎn)為一個(gè)普通指針睦擂。通過(guò)普通指針來(lái)更新值:

func main() {
  x := 2
  d := reflect.ValueOf(&x).Elem()
  px := d.Addr().Interface().(*int)
  *px = 3
  fmt.Println(x) // 3
}

這樣的更新方法有點(diǎn)麻煩得湘,我們可以直接通過(guò)可尋址的reflect.Value調(diào)用Set()方法來(lái)更新,不用通過(guò)指針:

d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4

如果傳入的類型不匹配顿仇,會(huì) panic淘正。reflect.Value為基本類型提供特殊的Set方法:SetIntSetUint臼闻、SetFloat等:

d.SetInt(5)
fmt.Println(x) // 5

反射可以讀取未導(dǎo)出結(jié)構(gòu)字段的值鸿吆,但是不能更新這些值。一個(gè)可尋址的reflect.Value會(huì)記錄它是否是通過(guò)遍歷一個(gè)未導(dǎo)出字段來(lái)獲得的些阅,如果是則不允許修改伞剑。所以在更新前使用CanAddr()判斷并不保險(xiǎn)斑唬。CanSet()可以正確判斷一個(gè)值是否可以修改市埋。

CanSet()判斷的是可設(shè)置性,它是比可尋址性更嚴(yán)格的性質(zhì)恕刘。如果一個(gè)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}
// 報(bào)錯(cuò)
// age.SetInt(20)

StructTag

在定義結(jié)構(gòu)體時(shí)褐着,可以為每個(gè)字段指定一個(gè)標(biāo)簽坷澡,我們可以使用反射讀取這些標(biāo)簽:

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)
  }
}

標(biāo)簽就是一個(gè)普通的字符串,上面程序輸出:

json:"name"
json:"age"

StructTag定義在reflect/type.go文件中:

type StructTag string

一般慣例是將各個(gè)鍵值對(duì)含蓉,使用空格分開(kāi)频敛,鍵值之間使用:。例如:

`json:"name" xml:"age"`

StructTag提供Get()方法獲取鍵對(duì)應(yīng)的值馅扣。

總結(jié)

本文系統(tǒng)地介紹了 Go 語(yǔ)言中的反射機(jī)制斟赚,從類型、接口到反射用法差油。還使用反射實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的基于 HTTP 的 RPC 庫(kù)拗军。反射雖然在平時(shí)開(kāi)發(fā)中不建議使用任洞,但是閱讀源碼,自己編寫(xiě)庫(kù)的時(shí)候需要頻繁用到反射知識(shí)发侵。熟練掌握反射可以使源碼閱讀事半功倍交掏。

大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù)刃鳄,歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??

參考

  1. Rob Pike, laws of reflection: https://golang.org/doc/articles/laws_of_reflection.html
  2. Go 程序設(shè)計(jì)語(yǔ)言盅弛,第 12 章:反射
  3. reflect 官方文檔,https://pkg.go.dev/reflect
  4. Go 每日一庫(kù) GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】叔锐,共同學(xué)習(xí)熊尉,一起進(jìn)步~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掌腰,隨后出現(xiàn)的幾起案子狰住,更是在濱河造成了極大的恐慌,老刑警劉巖齿梁,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件催植,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡勺择,警方通過(guò)查閱死者的電腦和手機(jī)创南,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)省核,“玉大人稿辙,你說(shuō)我怎么就攤上這事∑遥” “怎么了邻储?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)旧噪。 經(jīng)常有香客問(wèn)我吨娜,道長(zhǎng),這世上最難降的妖魔是什么淘钟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任宦赠,我火速辦了婚禮,結(jié)果婚禮上米母,老公的妹妹穿的比我還像新娘勾扭。我一直安慰自己,他們只是感情好铁瞒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布妙色。 她就那樣靜靜地躺著,像睡著了一般精拟。 火紅的嫁衣襯著肌膚如雪燎斩。 梳的紋絲不亂的頭發(fā)上虱歪,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音栅表,去河邊找鬼笋鄙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怪瓶,可吹牛的內(nèi)容都是我干的萧落。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼洗贰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼找岖!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起敛滋,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤许布,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绎晃,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蜜唾,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年庶艾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了袁余。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咱揍,死狀恐怖颖榜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情煤裙,我是刑警寧澤掩完,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站积暖,受9級(jí)特大地震影響藤为,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜夺刑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望分别。 院中可真熱鬧遍愿,春花似錦、人聲如沸耘斩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)括授。三九已至坞笙,卻和暖如春岩饼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背薛夜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工籍茧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梯澜。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓寞冯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親晚伙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吮龄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 一、認(rèn)識(shí)反射 維基百科中的定義:在計(jì)算機(jī)科學(xué)中咆疗,反射是指計(jì)算機(jī)程序在運(yùn)行時(shí)(Run time)可以訪問(wèn)漓帚、檢測(cè)和修改...
    Every_dawn閱讀 1,574評(píng)論 0 0
  • reflection 反射(reflection)是程序在運(yùn)行時(shí)通過(guò)檢查其定義的變量和值獲得對(duì)應(yīng)的真實(shí)類型。 在計(jì)...
    JunChow520閱讀 1,755評(píng)論 0 5
  • 轉(zhuǎn)載自: Go Reflect 最近在看一些go語(yǔ)言標(biāo)準(zhǔn)庫(kù)以及第三方庫(kù)的源碼時(shí)午磁,發(fā)現(xiàn)go的reflect被大量使用...
    lucasdada閱讀 381評(píng)論 0 1
  • Go reflect 反射實(shí)例解析 ——教壞小朋友系列 0 FBI WARNING 對(duì)于本文內(nèi)容胰默,看懂即可,完全不...
    楊浥塵閱讀 690評(píng)論 0 3
  • Reflect 本文側(cè)重講解reflect反射的實(shí)踐應(yīng)用漓踢,適合新手初窺門徑牵署。 reflect兩個(gè)基本功能 refl...
    tinsonHo閱讀 370評(píng)論 0 0