Skip to content

反射

反射是一種在運行時檢查語言自身結構的機制,它可以很靈活的去應對一些問題,但同時帶來的弊端也很明顯,例如性能問題等等。在 Go 中,反射與interface{}密切相關,很大程度上,只要有interface{}出現的地方,就會有反射。Go 中的反射 API 是由標准庫reflect包提供的。

接口

在開始之前先簡單的了解一下位於runtime包下的兩個接口。在 Go 中,接口本質上是結構體,Go 在運行時將接口分為了兩大類,一類是沒有方法集的接口,另一個類則是有方法集的接口。對於含有方法集的接口來說,在運行時由如下的結構體iface來進行表示

go
type iface struct {
   tab  *itab // 包含 數據類型,接口類型,方法集等
   data unsafe.Pointer // 指向值的指針
}

而對於沒有方法集接口來說,在運行時由eface 結構體來進行表示,如下

go
type eface struct {
   _type *_type // 類型
   data  unsafe.Pointer // 指向值的指針
}

而這兩個結構體在reflect包下都有與其對應的結構體類型,iface對應的是nonEmptyInterface

go
type nonEmptyInterface struct {
  itab *struct {
    ityp *rtype // 靜態接口類型
    typ  *rtype // 動態具體類型
    hash uint32 // 類型哈希
    _    [4]byte
    fun  [100000]unsafe.Pointer // 方法集
  }
  word unsafe.Pointer // 指向值的指針
}

eface對應的是emptyInterface

go
type emptyInterface struct {
   typ  *rtype // 動態具體類型
   word unsafe.Pointer // 指向指針的值
}

對於這兩種類型,官方給出了很明確的定義

  • nonEmptyInterface: nonEmptyInterface is the header for an interface value with methods
  • emptyInterface:emptyInterface is the header for an interface{} value

上述提到了動態具體類型這一詞,原文為dynamic concrete type,首先 Go 語言是一個百分之百的靜態類型語言,靜態這一詞是體現在對外表現的抽象的接口類型是不變的,而動態表示是接口底層存儲的具體實現的類型是可以變化的。至此,對於接口的簡單原理只需要了解到這裡就足夠滿足後續反射的學習。

橋梁

reflect包下,分別有reflect.Type接口類型來表示 Go 中的類型,reflect.Value結構體類型來表示 Go 中的值

go
type Type interface {
    ...

    Name() string

  PkgPath() string

  Size() uintptr

  String() string

  Kind() Kind

    ...
}

type Value struct {

   typ *rtype

   ptr unsafe.Pointer

   flag

}

上面的代碼省略了很多細節,先只需要了解這兩個類型的存在即可。Go 中所有反射相關的操作都是基於這兩個類型,reflect包提供了兩個函數來將 Go 中的類型轉換為上述的兩種類型以便進行反射操作,分別是reflect.TypeOf函數

go
func TypeOf(i any) Type

reflect.ValueOf函數

go
func ValueOf(i any) Value

可以看到兩個函數的參數類型都是any,也就是interface{}的別名。如果想要進行反射操作,就需要先將其類型轉換為interface{},這也是為什麼前面提到了只要有反射就離不開空接口。不嚴謹的說,空接口就是連接 Go 類型系統與反射的橋梁,下圖很形象的描述了其過程。

TIP

下文中為了方便,統一使用別名any來替代interface{}

核心

在 Go 中有三個經典的反射定律,結合上面所講的內容也就非常好懂,分別如下

  1. 反射可以將interface{}類型變量轉換成反射對象

  2. 反射可以將反射對象還原成interface{}類型變量

  3. 要修改反射對象,其值必須是可設置的

這三個定律便是 Go 反射的核心,當需要訪問類型相關信息時,便需要用到reflect.TypeOf,當需要修改反射值時,就需要用到reflect.ValueOf

類型

reflect.Type代表著 Go 中的類型,使用reflect.TypeOf()函數可以將變量轉換成reflect.Type。代碼示例如下

go
func main() {
  str := "hello world!"
  reflectType := reflect.TypeOf(str)
  fmt.Println(reflectType)
}

輸出結果為

string

Kind

對於Type而言,Go 內部使用reflect.Kind來表示 Go 中的基礎類型,其本質上是無符號整型uint

go
type Kind uint

reflect包使用Kind枚舉出了 Go 中所有的基礎類型,如下所示

go
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
   Pointer
   Slice
   String
   Struct
   UnsafePointer
)

Kind類型僅僅實現了Stringer接口的String()方法,該類型也僅有這一個方法,String()方法的返回值來自於一個其內部的 slice,如下所示,這種寫法乍一看很像 map,但其實這是 Go 中的一種特殊的寫法:索引初始化語法(index expressions in slice literals)

go
var kindNames = []string{
   Invalid:       "invalid",
   Bool:          "bool",
   Int:           "int",
   Int8:          "int8",
   Int16:         "int16",
   Int32:         "int32",
   Int64:         "int64",
   Uint:          "uint",
   Uint8:         "uint8",
   Uint16:        "uint16",
   Uint32:        "uint32",
   Uint64:        "uint64",
   Uintptr:       "uintptr",
   Float32:       "float32",
   Float64:       "float64",
   Complex64:     "complex64",
   Complex128:    "complex128",
   Array:         "array",
   Chan:          "chan",
   Func:          "func",
   Interface:     "interface",
   Map:           "map",
   Pointer:       "ptr",
   Slice:         "slice",
   String:        "string",
   Struct:        "struct",
   UnsafePointer: "unsafe.Pointer",
}
go
type Type interface{
    Kind() Kind
}

通過Kind,可以知曉空接口存儲的值究竟是什麼基礎類型,例如

go
func main() {
    // 聲明一個any類型的變量
  var eface any
    // 賦值
  eface = 100
    // 通過Kind方法,來獲取其類型
  fmt.Println(reflect.TypeOf(eface).Kind())
}

輸出結果

int

Elem

go
type Type interface{
    Elem() Type
}

使用Type.Elem()方法,可以判斷類型為any的數據結構所存儲的元素類型,可接收的底層參數類型必須是指針,切片,數組,通道,映射表其中之一,否則會panic。下面是代碼示例

go
func main() {
  var eface any
  eface = map[string]int{}
  rType := reflect.TypeOf(eface)
    // key()會返回map的鍵反射類型
  fmt.Println(rType.Key().Kind())
  fmt.Println(rType.Elem().Kind())
}

輸出為

string
int

指針也可以理解為是一個容器,對於指針使用Elem()會獲得其指向元素的反射類型,代碼示例如下

go
func main() {
  var eface any
    // 賦值指針
  eface = new(strings.Builder)
  rType := reflect.TypeOf(eface)
    // 拿到指針所指向元素的反射類型
  vType := rType.Elem()
    // 輸出包路徑
  fmt.Println(vType.PkgPath())
    // 輸出其名稱
  fmt.Println(vType.Name())
}
strings
Builder

對於數組,切片,通道用使用起來都是類似的。

Size

go
type Type interface{
    Size() uintptr
}

通過Size方法可以獲取對應類型所佔的字節大小,示例如下

go
func main() {
  fmt.Println(reflect.TypeOf(0).Size())
  fmt.Println(reflect.TypeOf("").Size())
  fmt.Println(reflect.TypeOf(complex(0, 0)).Size())
  fmt.Println(reflect.TypeOf(0.1).Size())
  fmt.Println(reflect.TypeOf([]string{}).Size())
}

輸出結果為

8
16
16
8
24

TIP

使用unsafe.Sizeof()可以達到同樣的效果

Comparable

go
type Type interface{
    Comparable() bool
}

通過Comparable方法可以判斷一個類型是否可以被比較,例子如下

go
func main() {
  fmt.Println(reflect.TypeOf("hello world!").Comparable())
  fmt.Println(reflect.TypeOf(1024).Comparable())
  fmt.Println(reflect.TypeOf([]int{}).Comparable())
  fmt.Println(reflect.TypeOf(struct{}{}).Comparable())
}

輸出如下

true
true
false
true

Implements

go
type Type interface{
    Implements(u Type) bool
}

通過Implements方法可以判斷一個類型是否實現了某一接口

go
type MyInterface interface {
  My() string
}

type MyStruct struct {
}

func (m MyStruct) My() string {
  return "my"
}

type HisStruct struct {
}

func (h HisStruct) String() string {
  return "his"
}

func main() {
  rIface := reflect.TypeOf(new(MyInterface)).Elem()
  fmt.Println(reflect.TypeOf(new(MyStruct)).Elem().Implements(rIface))
  fmt.Println(reflect.TypeOf(new(HisStruct)).Elem().Implements(rIface))
}

輸出結果

true
false

ConvertibleTo

go
type Type interface{
    ConvertibleTo(u Type) bool
}

使用ConvertibleTo方法可以判斷一個類型是否可以被轉換為另一個指定的類型

go
type MyInterface interface {
  My() string
}

type MyStruct struct {
}

func (m MyStruct) My() string {
  return "my"
}

type HisStruct struct {
}

func (h HisStruct) String() string {
  return "his"
}

func main() {
  rIface := reflect.TypeOf(new(MyInterface)).Elem()
  fmt.Println(reflect.TypeOf(new(MyStruct)).Elem().ConvertibleTo(rIface))
  fmt.Println(reflect.TypeOf(new(HisStruct)).Elem().ConvertibleTo(rIface))
}

輸出

true
false

reflect.Value代表著反射接口的值,使用reflect.ValueOf()函數可以將變量轉換成reflect.Value。代碼示例如下

go
func main() {
  str := "hello world!"
  reflectValue := reflect.ValueOf(str)
  fmt.Println(reflectValue)
}

輸出結果為

hello world!

Type

go
func (v Value) Type() Type

Type方法可以獲取一個反射值的類型

go
func main() {
   num := 114514
   rValue := reflect.ValueOf(num)
   fmt.Println(rValue.Type())
}

輸出

int

Elem

go
func (v Value) Elem() Value

獲取一個反射值的元素反射值

go
func main() {
   num := new(int)
   *num = 114514
   // 以指針為例子
   rValue := reflect.ValueOf(num).Elem()
   fmt.Println(rValue.Interface())
}

輸出

114514

指針

獲取一個反射值的指針方式有兩種

go
// 返回一個表示v地址的指針反射值
func (v Value) Addr() Value

// 返回一個指向v的原始值的uinptr 等價於 uintptr(Value.Addr().UnsafePointer())
func (v Value) UnsafeAddr() uintptr

// 返回一個指向v的原始值的uintptr
// 僅當v的Kind為 Chan, Func, Map, Pointer, Slice, UnsafePointer時,否則會panic
func (v Value) Pointer() uintptr

// 返回一個指向v的原始值的unsafe.Pointer
// 僅當v的Kind為 Chan, Func, Map, Pointer, Slice, UnsafePointer時,否則會panic
func (v Value) UnsafePointer() unsafe.Pointer

示例如下

go
func main() {
   num := 1024
   ele := reflect.ValueOf(&num).Elem()
   fmt.Println("&num", &num)
   fmt.Println("Addr", ele.Addr())
   fmt.Println("UnsafeAddr", unsafe.Pointer(ele.UnsafeAddr()))
   fmt.Println("Pointer", unsafe.Pointer(ele.Addr().Pointer()))
   fmt.Println("UnsafePointer", ele.Addr().UnsafePointer())
}

輸出

&num 0xc0000a6058
Addr 0xc0000a6058
UnsafeAddr 0xc0000a6058
Pointer 0xc0000a6058
UnsafePointer 0xc0000a6058

TIP

fmt.Println會反射獲取參數的類型,如果是reflect.Value類型的話,會自動調用Value.Interface()來獲取其原始值。

換成一個 map 再來一遍

go
func main() {
  dic := map[string]int{}
  ele := reflect.ValueOf(&dic).Elem()
  println(dic)
  fmt.Println("Addr", ele.Addr())
  fmt.Println("UnsafeAddr", *(*unsafe.Pointer)(unsafe.Pointer(ele.UnsafeAddr())))
  fmt.Println("Pointer", unsafe.Pointer(ele.Pointer()))
  fmt.Println("UnsafePointer", ele.UnsafePointer())
}

輸出

0xc00010e4b0
Addr &map[]
UnsafeAddr 0xc00010e4b0
Pointer 0xc00010e4b0
UnsafePointer 0xc00010e4b0

設置值

go
func (v Value) Set(x Value)

倘若通過反射來修改反射值,那麼其值必須是可取址的,這時應該通過指針來修改其元素值,而不是直接嘗試修改元素的值。

go
func main() {
   // *int
   num := new(int)
   *num = 114514
   rValue := reflect.ValueOf(num)
    // 獲取指針指向的元素
   ele := rValue.Elem()
   fmt.Println(ele.Interface())
   ele.SetInt(11)
   fmt.Println(ele.Interface())
}

輸出如下

114514
11

獲取值

go
func (v Value) Interface() (i any)

通過Interface()方法可以獲取反射值原有的值

go
func main() {
   var str string
   str = "hello"
   rValue := reflect.ValueOf(str)
   if v, ok := rValue.Interface().(string); ok {
      fmt.Println(v)
   }
}

輸出

hello

函數

通過反射可以獲取函數的一切信息,也可以反射調用函數

信息

通過反射類型來獲取函數的一切信息

go
func Max(a, b int) int {
   if a > b {
      return a
   }
   return b
}

func main() {
   rType := reflect.TypeOf(Max)
   // 輸出函數名稱,字面量函數的類型沒有名稱
   fmt.Println(rType.Name())
   // 輸出參數,返回值的數量
   fmt.Println(rType.NumIn(), rType.NumOut())
   rParamType := rType.In(0)
   // 輸出第一個參數的類型
   fmt.Println(rParamType.Kind())
   rResType := rType.Out(0)
   // 輸出第一個返回值的類型
   fmt.Println(rResType.Kind())
}

輸出


2 1
int
int

調用

通過反射值來調用函數

go
func (v Value) Call(in []Value) []Value
go
func main() {
   // 獲取函數的反射值
   rType := reflect.ValueOf(Max)
   // 傳入參數數組
   rResValue := rType.Call([]reflect.Value{reflect.ValueOf(18), reflect.ValueOf(50)})
   for _, value := range rResValue {
      fmt.Println(value.Interface())
   }
}

輸出

50

結構體

假設有如下結構體

go
type Person struct {
  Name    string `json:"name"`
  Age     int    `json:"age"`
  Address string `json:"address"`
  money   int
}

func (p Person) Talk(msg string) string {
  return msg
}

訪問字段

reflect.StructField結構的結構如下

go
type StructField struct {
  // 字段名稱
  Name string
  // 包名
  PkgPath string
  // 類型名
  Type      Type
  // Tag
  Tag       StructTag
  // 字段的字節偏移
  Offset    uintptr
  // 索引
  Index     []int
  // 是否為嵌套字段
  Anonymous bool
}

訪問結構體字段的方法有兩種,一種是通過索引來進行訪問,另一種是通過名稱。

go
type Type interface{
    Field(i int) StructField
}

通過索引訪問的例子如下

go
func main() {
  rType := reflect.TypeOf(new(Person)).Elem()
  // 輸出結構體字段的數量
  fmt.Println(rType.NumField())
  for i := 0; i < rType.NumField(); i++ {
    structField := rType.Field(i)
    fmt.Println(structField.Index, structField.Name, structField.Type, structField.Offset, structField.IsExported())
  }
}

輸出

4
[0] Name string 0 true
[1] Age int 16 true
[2] Address string 24 true
[3] money int 40 false
go
type Type interface{
    FieldByName(name string) (StructField, bool)
}

通過名稱訪問的例子如下

go
func main() {
   rType := reflect.TypeOf(new(Person)).Elem()
   // 輸出結構體字段的數量
   fmt.Println(rType.NumField())
   if field, ok := rType.FieldByName("money"); ok {
      fmt.Println(field.Name, field.Type, field.IsExported())
   }
}

輸出

4
money int false

修改字段

倘若要修改結構體字段值,則必須傳入一個結構體指針,下面是一個修改字段的例子

go
func main() {
  // 傳入指針
  rValue := reflect.ValueOf(&Person{
    Name:    "",
    Age:     0,
    Address: "",
    money:   0,
  }).Elem()

  // 獲取字段
  name := rValue.FieldByName("Name")
  // 修改字段值
  if (name != reflect.Value{}) { // 如果返回reflect.Value{},則說明該字段不存在
    name.SetString("jack")
  }
  // 輸出結構體
  fmt.Println(rValue.Interface())
}

輸出

{jack 0  0}

對於修改結構體私有字段而言,需要進行一些額外的操作,如下

go
func main() {
  // 傳入指針
  rValue := reflect.ValueOf(&Person{
    Name:    "",
    Age:     0,
    Address: "",
    money:   0,
  }).Elem()

  // 獲取一個私有字段
  money := rValue.FieldByName("money")
  // 修改字段值
  if (money != reflect.Value{}) {
    // 構造指向該結構體未導出字段的指針反射值
    p := reflect.NewAt(money.Type(), money.Addr().UnsafePointer())
    // 獲取該指針所指向的元素,也就是要修改的字段
    field := p.Elem()
    // 修改值
    field.SetInt(164)
  }
  // 輸出結構體
  fmt.Printf("%+v\n", rValue.Interface())
}

訪問 Tag

獲取到StructField後,便可以直接訪問其 Tag

go
// 如果不存在,ok為false
func (tag StructTag) Lookup(key string) (value string, ok bool)

// 如果不存在,返回空字符串
func (tag StructTag) Get(key string) string

示例如下

go
func main() {
   rType := reflect.TypeOf(new(Person)).Elem()
   name, ok := rType.FieldByName("Name")
   if ok {
      fmt.Println(name.Tag.Lookup("json"))
      fmt.Println(name.Tag.Get("json"))
   }
}

輸出

name true
name

訪問方法

訪問方法與訪問字段的過程很相似,只是函數簽名略有區別。reflect.Method結構體如下

go
type Method struct {
  // 方法名
  Name string
  // 包名
  PkgPath string
  // 方法類型
  Type  Type
  // 方法對應的函數,第一個參數是接收者
  Func  Value
  // 索引
  Index int
}

訪問方法信息示例如下

go
func main() {
  // 獲取結構體反射類型
  rType := reflect.TypeOf(new(Person)).Elem()
  // 輸出方法個數
  fmt.Println(rType.NumMethod())
  // 遍歷輸出方法信息
  for i := 0; i < rType.NumMethod(); i++ {
    method := rType.Method(i)
    fmt.Println(method.Index, method.Name, method.Type, method.IsExported())
  }
}

輸出

1
0 Talk func(main.Person, string) string true

如果想要獲取方法的參數和返回值細節,可以通過Method.Func來進行獲取,過程與訪問函數信息一致,將上面的代碼稍微修改下

go
func main() {
  // 獲取結構體反射類型
  rType := reflect.TypeOf(new(Person)).Elem()
  // 輸出方法個數
  fmt.Println(rType.NumMethod())
  // 遍歷輸出方法信息
  for i := 0; i < rType.NumMethod(); i++ {
    method := rType.Method(i)
    fmt.Println(method.Index, method.Name, method.Type, method.IsExported())
    fmt.Println("方法參數")
    for i := 0; i < method.Func.Type().NumIn(); i++ {
      fmt.Println(method.Func.Type().In(i).String())
    }
    fmt.Println("方法返回值")
    for i := 0; i < method.Func.Type().NumOut(); i++ {
      fmt.Println(method.Func.Type().Out(i).String())
    }
  }
}

可以看到第一個參數是main.Person,也就是接收者類型

1
0 Talk func(main.Person, string) string true
方法參數
main.Person
string
方法返回值
string

調用方法

調用方法與調用函數的過程相似,而且並不需要手動傳入接收者,例子如下

go
func main() {
   // 獲取結構體反射類型
   rValue := reflect.ValueOf(new(Person)).Elem()
   // 輸出方法個數
   fmt.Println(rValue.NumMethod())
   // 遍歷輸出方法信息
   talk := rValue.MethodByName("Talk")
   if (talk != reflect.Value{}) {
      // 調用方法,並獲取返回值
      res := talk.Call([]reflect.Value{reflect.ValueOf("hello,reflect!")})
      // 遍歷輸出返回值
      for _, re := range res {
         fmt.Println(re.Interface())
      }
   }
}

輸出

1
hello,reflect!

創建

通過反射可以構造新的值,reflect包同時根據一些特殊的類型提供了不同的更為方便的函數。

基本類型

go
// 返回指向反射值的指針反射值
func New(typ Type) Value

string為例

go
func main() {
   rValue := reflect.New(reflect.TypeOf(*new(string)))
   rValue.Elem().SetString("hello world!")
   fmt.Println(rValue.Elem().Interface())
}
hello world!

結構體

結構體的創建同樣用到reflect.New函數

go
type Person struct {
   Name    string `json:"name"`
   Age     int    `json:"age"`
   Address string `json:"address"`
   money   int
}

func (p Person) Talk(msg string) string {
   return msg
}

func main() {
   // 創建結構體反射值
   rType := reflect.TypeOf(new(Person)).Elem()
   person := reflect.New(rType).Elem()
   fmt.Println(person.Interface())
}

輸出

{ 0  0}

切片

反射創建切片

go
func MakeSlice(typ Type, len, cap int) Value
go
func main() {
   // 創建切片反射值
   rValue := reflect.MakeSlice(reflect.TypeOf(*new([]int)), 10, 10)
   // 遍歷賦值
   for i := 0; i < 10; i++ {
      rValue.Index(i).SetInt(int64(i))
   }
   fmt.Println(rValue.Interface())
}
[0 1 2 3 4 5 6 7 8 9]

Map

反射創建 Map

go
func MakeMapWithSize(typ Type, n int) Value
go
func main() {
   //構建map反射值
   rValue := reflect.MakeMapWithSize(reflect.TypeOf(*new(map[string]int)), 10)
   // 設置值
   rValue.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(1))
   fmt.Println(rValue.Interface())
}
map[a:1]

管道

反射創建管道

go
func MakeChan(typ Type, buffer int) Value
go
func main() {
   // 創建管道反射值
   makeChan := reflect.MakeChan(reflect.TypeOf(new(chan int)).Elem(), 0)
   fmt.Println(makeChan.Interface())
}

函數

反射創建函數

go
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
go
func main() {
    // 傳入包裝類型和函數體
  fn := reflect.MakeFunc(reflect.TypeOf(new(func(int))).Elem(), func(args []reflect.Value) (results []reflect.Value) {
    for _, arg := range args {
      fmt.Println(arg.Interface())
    }
    return nil
  })
  fmt.Println(fn.Type())
  fn.Call([]reflect.Value{reflect.ValueOf(1024)})
}

輸出

func(int)
1024

完全相等

reflect.DeepEqual是反射包下提供的一個用於判斷兩個變量是否完全相等的函數,簽名如下。

go
func DeepEqual(x, y any) bool

該函數對於每一種基礎類型都做了處理,下面是一些類型判斷方式。

  • 數組:數組中的每一個元素都完全相等
  • 切片:都為nil時,判為完全相等,或者都不為空時,長度范圍內的元素完全相等
  • 結構體:所有字段都完全相等
  • 映射表:都為nil時,為完全相等,都不為nil時,每一個鍵所映射的值都完全相等
  • 指針:指向同一個元素或指向的元素完全相等
  • 接口:接口的具體類型完全相等時
  • 函數:只有兩者都為nil時才是完全相等,否則就不是完全相等

下面是一些例子:

切片

go
func main() {
   a := make([]int, 100)
   b := make([]int, 100)
   fmt.Println(reflect.DeepEqual(a, b))
}

輸出

true

結構體

go
func main() {
   mike := Person{
      Name:   "mike",
      Age:    39,
      Father: nil,
   }

   jack := Person{
      Name:   "jack",
      Age:    18,
      Father: &mike,
   }

   tom := Person{
      Name:   "tom",
      Age:    18,
      Father: &mike,
   }
   fmt.Println(reflect.DeepEqual(mike, jack))
   fmt.Println(reflect.DeepEqual(tom, jack))
   fmt.Println(reflect.DeepEqual(jack, jack))
}

輸出

false
false
true

Golang學習網由www.golangdev.cn整理維護