Go 語言反射編程指南

反射 [1] 是一種編程語言的高級特性,它允許程序在運行時檢視自身的結構和行爲。通過反射,程序可以動態地獲取類型 (type) 與值 (value) 等信息,並對它們進行操作,諸如修改字段、調用方法等,這使得程序具有更大的靈活性和可擴展性。

不過,反射雖然具有強大的功能,但也存在一些缺點。由於反射是在運行時進行的,因此它比直接調用代碼的性能要差。此外,反射還可能導致代碼的可讀性和維護性降低,因爲它使得程序行爲更加難以預測和理解。因此,在使用反射時需要注意性能和可維護性。

Go 從誕生伊始就在運行時支持了反射,並在標準庫中提供了 reflect 包供開發者進行反射編程時使用。在這篇文章中,我們就來系統地瞭解一下如何在 Go 中通過 reflect 包實現反射編程。

  1. Go 語言反射基礎

相對於 C/C++ 等系統編程語言,Go 的運行時承擔的功能要更多一些,比如 Goroutine 調度 [3]、Go 內存垃圾回收 (GC)[4] 等。同時反射也爲開發者與運行時之間提供了一個方便的、合法的交互窗口。通過反射,開發者可以合法的窺探關於 Go 類型系統的一些元信息。

注:《Go 語言第一課》[5] 專欄第 31~34 講對 Goroutine 調度以及 Go 併發編程做了系統詳細的講解,歡迎閱讀。

Go 語言的反射包(reflect 包)是一個內置的包,它提供了一組 API,能夠在運行時獲取和修改 Go 語言程序的結構和行爲。reflect 包也是所有 Go 反射編程的基礎 API,是進行 Go 反射編程的必經之路。

在本節中,我們將會探討 reflect 包的一些基礎知識,包括 Type 和 Value 兩個重要的反射包類型,以及如何使用 TypeOf 和 ValueOf 方法來獲取類型信息和值信息。

1.1 Type 和 Value

在 reflect 包中,Type 和 Value 是兩個非常重要的概念,它們分別表示了反射世界中的類型信息和值信息。

Type 表示一個類型的元信息,它包含了類型的名稱、大小、方法集合等信息。在反射編程中,我們可以使用 TypeOf 函數來獲取一個值的類型信息。

Value 表示一個值的信息,它包含了值的類型、值本身以及對值進行操作的方法集合等信息。在反射中,我們可以使用 ValueOf 函數來獲取一個值的 Value 信息。

reflect 包的 TypeOf 和 ValueOf 兩個函數是進入反射世界的基本入口。下面我們來看看這兩個函數的基本用法示例。

1.2 如何獲取類型信息(TypeOf)

獲取類型信息是反射的一個重要功能。在 Go 語言中,我們可以使用 reflect 包的 TypeOf 函數來獲取一個值的類型信息。TypeOf 函數的簽名如下:

func TypeOf(i any) Type

注:any[6] 是 interface{} 的 alias type,是 Go 1.18[7] 中引入的預定義標識符。

TypeOf 函數接受一個任意類型的值作爲參數,並返回該值的類型信息,即 interface{} 接口類型變量中存儲的動態類型信息。例如,我們可以使用 TypeOf 函數獲取一個字符串的類型信息:

import (
    "fmt"
    "reflect"
)

func main() {
    s := "hello, world!"
    t := reflect.TypeOf(s)
    fmt.Println(t.Name()) // string
}

用圖直觀表示如下:

1.4 如何獲取值信息(ValueOf)

獲取值信息是反射的另一個重要功能。在 Go 語言中,我們可以使用 reflect 包的 ValueOf 函數來獲取一個值的 Value 信息。ValueOf 函數的簽名如下:

func ValueOf(i any) Value

ValueOf 函數接受一個任意類型的值作爲參數,並返回該值的 Value 信息,即 interface{} 接口類型變量中存儲的動態類型的值的信息。例如,我們可以使用 ValueOf 函數獲取一個整數的 Value 信息:

import (
    "fmt"
    "reflect"
)

func main() {
    i := 42
    v := reflect.ValueOf(i)
    fmt.Println(v.Int()) // 42
}

在上述示例中,我們首先定義了一個整數 i,然後使用 ValueOf 函數獲取其 Value 信息,並調用 Int 方法獲取其值。

用圖直觀表示如下:

以上就是 reflect 包 TypeOf 和 ValueOf 函數的基本用法的示例,下面我們再來詳細看看獲取不同類型的類型信息和值信息的細節。

  1. 檢視類型信息和調用類型方法

reflect.Type 實質上是一個接口類型,它封裝了 reflect 可以提供的類型信息的所有方法 (Go 1.20 版本中的 reflect.Type):

// $GOROOT/src/reflect/type.go

type Type interface {
 // Methods applicable to all types.

 // Align returns the alignment in bytes of a value of
 // this type when allocated in memory.
 Align() int

 // FieldAlign returns the alignment in bytes of a value of
 // this type when used as a field in a struct.
 FieldAlign() int

 // Method returns the i'th method in the type's method set.
 // It panics if i is not in the range [0, NumMethod()).
 //
 // For a non-interface type T or *T, the returned Method's Type and Func
 // fields describe a function whose first argument is the receiver,
 // and only exported methods are accessible.
 //
 // For an interface type, the returned Method's Type field gives the
 // method signature, without a receiver, and the Func field is nil.
 //
 // Methods are sorted in lexicographic order.
 Method(int) Method

 // MethodByName returns the method with that name in the type's
 // method set and a boolean indicating if the method was found.
 //
 // For a non-interface type T or *T, the returned Method's Type and Func
 // fields describe a function whose first argument is the receiver.
 //
 // For an interface type, the returned Method's Type field gives the
 // method signature, without a receiver, and the Func field is nil.
 MethodByName(string) (Method, bool)

 // NumMethod returns the number of methods accessible using Method.
 //
 // For a non-interface type, it returns the number of exported methods.
 //
 // For an interface type, it returns the number of exported and unexported methods.
 NumMethod() int

 // Name returns the type's name within its package for a defined type.
 // For other (non-defined) types it returns the empty string.
 Name() string

 // PkgPath returns a defined type's package path, that is, the import path
 // that uniquely identifies the package, such as "encoding/base64".
 // If the type was predeclared (string, error) or not defined (*T, struct{},
 // []int, or A where A is an alias for a non-defined type), the package path
 // will be the empty string.
 PkgPath() string

 // Size returns the number of bytes needed to store
 // a value of the given type; it is analogous to unsafe.Sizeof.
 Size() uintptr

 // String returns a string representation of the type.
 // The string representation may use shortened package names
 // (e.g., base64 instead of "encoding/base64") and is not
 // guaranteed to be unique among types. To test for type identity,
 // compare the Types directly.
 String() string

 // Kind returns the specific kind of this type.
 Kind() Kind

 // Implements reports whether the type implements the interface type u.
 Implements(u Type) bool

 // AssignableTo reports whether a value of the type is assignable to type u.
 AssignableTo(u Type) bool

 // ConvertibleTo reports whether a value of the type is convertible to type u.
 // Even if ConvertibleTo returns true, the conversion may still panic.
 // For example, a slice of type []T is convertible to *[N]T,
 // but the conversion will panic if its length is less than N.
 ConvertibleTo(u Type) bool

 // Comparable reports whether values of this type are comparable.
 // Even if Comparable returns true, the comparison may still panic.
 // For example, values of interface type are comparable,
 // but the comparison will panic if their dynamic type is not comparable.
 Comparable() bool

 // Methods applicable only to some types, depending on Kind.
 // The methods allowed for each kind are:
 //
 // Int*, Uint*, Float*, Complex*: Bits
 // Array: Elem, Len
 // Chan: ChanDir, Elem
 // Func: In, NumIn, Out, NumOut, IsVariadic.
 // Map: Key, Elem
 // Pointer: Elem
 // Slice: Elem
 // Struct: Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField

 // Bits returns the size of the type in bits.
 // It panics if the type's Kind is not one of the
 // sized or unsized Int, Uint, Float, or Complex kinds.
 Bits() int

 // ChanDir returns a channel type's direction.
 // It panics if the type's Kind is not Chan.
 ChanDir() ChanDir

 // IsVariadic reports whether a function type's final input parameter
 // is a "..." parameter. If so, t.In(t.NumIn() - 1) returns the parameter's
 // implicit actual type []T.
 //
 // For concreteness, if t represents func(x int, y ... float64)then
 //
 // t.NumIn() == 2
 // t.In(0) is the reflect.Type for "int"
 // t.In(1) is the reflect.Type for "[]float64"
 // t.IsVariadic() == true
 //
 // IsVariadic panics if the type's Kind is not Func.
 IsVariadic() bool

 // Elem returns a type's element type.
 // It panics if the type's Kind is not Array, Chan, Map, Pointer, or Slice.
 Elem() Type

 // Field returns a struct type's i'th field.
 // It panics if the type's Kind is not Struct.
 // It panics if i is not in the range [0, NumField()).
 Field(i int) StructField

 // FieldByIndex returns the nested field corresponding
 // to the index sequence. It is equivalent to calling Field
 // successively for each index i.
 // It panics if the type's Kind is not Struct.
 FieldByIndex(index []int) StructField

 // FieldByName returns the struct field with the given name
 // and a boolean indicating if the field was found.
 FieldByName(name string) (StructField, bool)

 // FieldByNameFunc returns the struct field with a name
 // that satisfies the match function and a boolean indicating if
 // the field was found.
 //
 // FieldByNameFunc considers the fields in the struct itself
 // and then the fields in any embedded structs, in breadth first order,
 // stopping at the shallowest nesting depth containing one or more
 // fields satisfying the match function. If multiple fields at that depth
 // satisfy the match function, they cancel each other
 // and FieldByNameFunc returns no match.
 // This behavior mirrors Go's handling of name lookup in
 // structs containing embedded fields.
 FieldByNameFunc(match func(string) bool) (StructField, bool)

 // In returns the type of a function type's i'th input parameter.
 // It panics if the type's Kind is not Func.
 // It panics if i is not in the range [0, NumIn()).
 In(i int) Type

 // Key returns a map type's key type.
 // It panics if the type's Kind is not Map.
 Key() Type

 // Len returns an array type's length.
 // It panics if the type's Kind is not Array.
 Len() int

 // NumField returns a struct type's field count.
 // It panics if the type's Kind is not Struct.
 NumField() int

 // NumIn returns a function type's input parameter count.
 // It panics if the type's Kind is not Func.
 NumIn() int

 // NumOut returns a function type's output parameter count.
 // It panics if the type's Kind is not Func.
 NumOut() int

 // Out returns the type of a function type's i'th output parameter.
 // It panics if the type's Kind is not Func.
 // It panics if i is not in the range [0, NumOut()).
 Out(i int) Type

 common() *rtype
 uncommon() *uncommonType
}

我們看到這是一個 “超級接口”,嚴格來說並不符合 Go 接口設計的慣例。

注:Go 崇尚小接口。以 Type 接口爲例,可以對 Type 接口做進一步分解,分解成若干內聚的小接口,然後將 Type 看成小接口的組合。

對於不同類型,Type 接口的有些方法是冗餘的,比如像上面的 NumField、NumIn 和 NumOut 方法對於一個 int 變量的類型信息來說就毫無意義。Type 類型的註釋中也提到:“Not all methods apply to all kinds of types”。

一旦通過 TypeOf 進入反射世界,拿到 Type 類型變量,那麼我們就可以基於上述方法 “翻看” 類型的各種信息了。

對於像 int、float64、string 這樣的基本類型來說,其類型信息的檢視沒有太多可說的。但對於其他類型,諸如複合類型、指針類型、函數類型等,還是有一些可聊聊的,我們下面逐一簡單地看一下。

2.1 複合類型

2.1.1 數組類型

在 Go 中,數組類型是一種典型的複合類型,它有若干屬性,包括數組長度、數組是否支持可比較、數組元素的類型等,看下面示例:

import (
    "fmt"
    "reflect"
)

func main() { 
    arr := [5]int{1, 2, 3, 4, 5}
    typ := reflect.TypeOf(arr)
    fmt.Println(typ.Kind())       // array
    fmt.Println(typ.Len())        // 5
    fmt.Println(typ.Comparable()) // true

    elemTyp := typ.Elem()
    fmt.Println(elemTyp.Kind())       // int
    fmt.Println(elemTyp.Comparable()) // true
}

注:通過類型信息無法間接得到值信息,反之不然,稍後系統說明 reflect.Value 時會提到。

在這個例子,我們輸出了 arr 這個數組類型變量的 Kind 信息。什麼是 Kind 信息呢?reflect 包中是如此定義的:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
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
    Pointer
    Slice
    String
    Struct
    UnsafePointer
)

我們可以將 Kind 當做是 Go type 信息的元信息,對於基本類型來說,如 int、string、float64 等,它的 kind 和它的 type 的表達是一致的。但對於像數組、切片等類型,kind 更像是 type 的 type。

以兩個數組類型爲例:

var arr1 [10]string
var arr2 [8]int

這兩個數組類型的類型分別是 [10]string 和 [8]int,但它們在反射世界的 reflect.Type 的 Kind 信息卻都爲 Array。

再比如下面兩個指針類型:

var p1 *float64
var p2 *MyFoo

這兩個指針類型的類型分別是 * float64 和 * MyFoo,但它們在反射世界的 reflect.Type 的 Kind 信息卻都爲 Pointer。

Kind 信息可以幫助開發人員在反射世界中區分類型,以對不同類型作不同的處理。比如對於 Kind 爲 Int 的 reflect.Type,你不能使用其 Len() 方法,否則會 panic;但對於 Kind 爲 Array 的則可以。開發人員使用反射提供的 Kind 信息可以處理不同類型的數據。

2.1.2 切片類型

在 Go 中切片是動態數組,可靈活、透明的擴容,多數情況下切片都能替代數組完成任務。在反射世界中通過 reflect.Type 我們可以獲取切片類型的信息,包括元素類型等。下面是一個示例:

package main

import ( 
    "fmt"
    "reflect"
)

func main() { 
    s := make([]int, 5, 10)
    typ := reflect.TypeOf(s)
    fmt.Println(typ.Kind()) // slice
    fmt.Println(typ.Elem()) // int
}

如果我們使用上面的變量 typ 調用 Type 類型的 Len 和 Cap 方法會發生什麼呢?在運行時,你將得到類似 "panic: reflect: Len of non-array type []int" 的報錯!

那麼問題來了!切片長度、容量到底是否是 slice type 的信息範疇呢? 我們來看一個例子:

var a = make([]int, 5, 10)
var b = make([]int, 7, 8)

變量 a 和 b 的類型都是 []int。顯然長度、容量等並不在切片類型的範疇,而是與切片變量值綁定的,下面的示例印證了這一點:

func main() {
    s := make([]int, 5, 10)
    val := reflect.ValueOf(s)
    fmt.Println(val.Len()) // 5
    fmt.Println(val.Cap()) // 10
}

我們獲取了切片變量 s 的 reflect.Value 信息,通過 Value 我們得到了變量 s 的長度和容量信息。

2.1.3 結構體類型

結構體類型是與反射聯合使用的重要類型,下面代碼展示瞭如何通過 reflect.Type 獲取結構體類型的相關信息:

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    gender  string
}

func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s, and I'm %d years old.\n", p.Name, p.Age)
}
func (p Person) unexportedMethod() {
}

func main() {
    p := Person{Name: "Tom", Age: 20, gender: "male"}
    typ := reflect.TypeOf(p)
    fmt.Println(typ.Kind())                   // struct
    fmt.Println(typ.NumField())               // 2
    fmt.Println(typ.Field(0).Name)            // Name
    fmt.Println(typ.Field(0).Type)            // string
    fmt.Println(typ.Field(0).Tag)             // json:"name"
    fmt.Println(typ.Field(1).Name)            // Age
    fmt.Println(typ.Field(1).Type)            // int
    fmt.Println(typ.Field(1).Tag)             // json:"age"
    fmt.Println(typ.Field(2).Name)            // gender
    fmt.Println(typ.Method(0).Name)           // SayHello
    fmt.Println(typ.Method(0).Type)           // func(main.Person)
    fmt.Println(typ.Method(0).Func)           // 0x109b6e0
    fmt.Println(typ.MethodByName("SayHello")) // {SayHello func(main.Person)}
    fmt.Println(typ.MethodByName("unexportedMethod")) // {  <nil> <invalid Value> 0} false
}

從上面例子可以看到,我們可以使用 NumField、Field、NumMethod、Method 和 MethodByName 等方法獲取結構體的字段信息和方法信息。其中,Field 方法返回的是 StructField 類型的值,包含了字段的名稱、類型、標籤等信息;Method 方法返回的是 Method 類型的值,包含了方法的名稱、類型和函數值等信息。

不過要注意:** 通過 Type 可以得到結構體中非導出字段的信息 (如上面示例中的 gender),但無法獲取結構體類型的非導出方法信息 (如上面示例中的 unexportedMethod)**。

2.1.4 channel 類型

channel 是 Go 特有的類型,channel 與切片很像,它的類型信息包括元素類型、chan 讀寫特性,但 channel 的長度與容量與 channel 變量是綁定的,看下面示例:

package main
  
import (
    "fmt"
    "reflect"
)

func main() {
    ch := make(chan<- int, 10)
    ch <- 1
    ch <- 2
    typ := reflect.TypeOf(ch)
    fmt.Println(typ.Kind())      // chan
    fmt.Println(typ.Elem())      // int
    fmt.Println(typ.ChanDir())   // chan<-

    fmt.Println(reflect.ValueOf(ch).Len()) // 2
    fmt.Println(reflect.ValueOf(ch).Cap()) // 10
}

基於反射和 channel 可以實現一些高級操作,比如之前寫過一篇《使用反射操作 channel》[8],大家可以移步看看。

2.1.5 map 類型

map 是 go 常用的內置的複合類型,它是一個無序鍵值對的集合,通過反射可以獲取其鍵和值的類型信息:

package main
  
import (
    "fmt"
    "reflect"
)

func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    typ := reflect.TypeOf(m)
    fmt.Println(typ.Kind()) // map        
    fmt.Println(typ.Key())  // string     
    fmt.Println(typ.Elem()) // int        

    fmt.Println(reflect.ValueOf(m).Len()) // 3
}

我們看到,和切片一樣,map 變量的長度信息是與 map 變量的 Value 綁定的,另外要注意:map 變量不能獲取容量信息

2.2 指針類型

指針類型是一個大類,通過 Type 可以獲得指針的 kind 和其指向的變量的類型信息:

package main
  
import (
    "fmt"
    "reflect"
)

func main() {
    i := 10
    p := &i
    typ := reflect.TypeOf(p)
    fmt.Println(typ.Kind())                      // ptr
    fmt.Println(typ.Elem())                      // int
}

2.3 接口類型

接口即契約。在 Go 中非作爲約束的接口類型本質就是一個方法集合,通過 reflect.Type 可以獲得接口類型的這些信息:

package main
  
import (
    "fmt"
    "reflect"
)

type Animal interface {
    Speak() string
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow"
}

func main() {
    var a Animal = Cat{}
    typ := reflect.TypeOf(a)
    fmt.Println(typ.Kind())         // interface
    fmt.Println(typ.NumMethod())    // 1
    fmt.Println(typ.Method(0).Name) // Speak
    fmt.Println(typ.Method(0).Type) // func(main.Animal) string
}

2.4 函數類型

函數在 Go 中是一等公民,我們可以將其像普通 int 類型那樣去使用,傳參、賦值、做返回值都是 ok 的。下面是通過 Type 獲取函數類型信息的示例:

package main

import ( 
    "fmt"
    "reflect"
)

func foo(a, b int, c *int) (int, bool) { 
    *c = a + b 
    return *c, true
}

func main() { 
    typ := reflect.TypeOf(foo)
    fmt.Println(typ.Kind())                      // func
    fmt.Println(typ.NumIn())                     // 3
    fmt.Println(typ.In(0), typ.In(1), typ.In(2)) // int int *int
    fmt.Println(typ.NumOut())                    // 2
    fmt.Println(typ.Out(0))                      // int
    fmt.Println(typ.Out(1))                      // bool
}

我們看到和其他類型不同,函數支持 NumOut、NumIn、Out 等方法。其中 In 是輸出參數的集合,Out 則是返回值參數的集合。

注:上述示例 foo 純粹爲了演示,不要計較其合理性問題。

  1. 獲取與修改值信息

掌握瞭如何在反射世界獲取一個變量的類型信息後,我們再來看看如何在反射世界獲取並修改一個變量的值信息。之前在《使用 reflect 包在反射世界裏讀寫各類型變量》[9] 一文中詳細講解了使用 reflect 讀寫變量的值信息,大家可以移步那篇文章閱讀。

注:並不是所有變量都可以修改值的,可以使用 Value 的 CanSet 方法判斷值是否可以設置。

  1. 調用函數與方法

通過反射我們可以在反射世界調用函數,也可以調用特定類型的變量的方法。

下面是一個通過 reflect.Value 調用函數的簡單例子:

package main
  
import (
    "fmt"
    "reflect"
)

func add(a, b int) int {
    return a + b
}

func main() {
    // 獲取函數類型變量
    val := reflect.ValueOf(add)
    // 準備函數參數
    args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
    // 調用函數
    result := val.Call(args)
    fmt.Println(result[0].Int()) // 輸出:3
}

從示例看到,我們通過 Value 的 Call 方法來調用函數 add。add 有兩個入參,我們不能直接傳入 int 類型,因爲這是在反射世界,我們要用反射世界的 “專用參數”,即 ValueOf 後的值。Call 的結果就是反射世界的返回值的 Value 形式,通過 Value.Int 方法可以還原反射世界的 Value 爲 int。

注:通過 reflect.Type 無法調用函數和方法。

方法的調用與函數調用類似,下面是一個例子:

import (
    "fmt"
    "reflect"
)

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area(factor float64) float64 {
    return r.Width * r.Height * factor
}

func main() {
    r := Rectangle{Width: 10, Height: 5}
    val := reflect.ValueOf(r)
    method := val.MethodByName("Area")
    args := []reflect.Value{reflect.ValueOf(1.5)}
    result := method.Call(args)
    fmt.Println(result[0].Float()) // 輸出:75
}

通過 MethodByName 獲取反射世界的 method value,然後同樣是通過 Call 方法實現方法 Area 的調用。

注:reflect 目前不支持對非導出方法的調用。

  1. 動態創建類型實例

reflect 更爲強大的功能是可以在運行時動態創建各種類型的實例。下面是在反射世界動態創建各種類型實例的示例。

5.1 基本類型

下面以 int、float64 和 string 爲例演示一下如何通過 reflect 在運行時動態創建基本類型的實例。

func main() {
    val := reflect.New(reflect.TypeOf(0))
    val.Elem().SetInt(42)
    fmt.Println(val.Elem().Int()) // 輸出:42
}
func main() {
    val := reflect.New(reflect.TypeOf(0.0))
    val.Elem().SetFloat(3.14)
    fmt.Println(val.Elem().Float()) // 輸出:3.14
}
func main() {
    val := reflect.New(reflect.TypeOf(""))
    val.Elem().SetString("hello")
    fmt.Println(val.Elem().String()) // 輸出:hello
}

更爲複雜的類型的實例,我們繼續往下看。

5.2 數組類型

使用 reflect 在運行時創建一個 [3]int 類型的數組實例,並設置數組實例各個元素的值:

func main() {
    typ := reflect.ArrayOf(3, reflect.TypeOf(0))
    val := reflect.New(typ)
    arr := val.Elem()
    arr.Index(0).SetInt(1)
    arr.Index(1).SetInt(2)
    arr.Index(2).SetInt(3)
    fmt.Println(arr.Interface()) // 輸出:[1 2 3]
    arr1, ok := arr.Interface().([3]int)
    if !ok {
        fmt.Println("not a [3]int")
        return
    }

    fmt.Println(arr1) // [1 2 3]
}

5.3 切片類型

使用 reflect 在運行時創建一個 []int 類型的切片實例,並設置切片實例中各個元素的值:

func main() {
    typ := reflect.SliceOf(reflect.TypeOf(0)) // 切片元素類型
    val := reflect.MakeSlice(typ, 3, 3) // 動態創建切片實例
    val.Index(0).SetInt(1)
    val.Index(1).SetInt(2)
    val.Index(2).SetInt(3)
    fmt.Println(val.Interface()) // 輸出:[1 2 3]

    sl, ok := val.Interface().([]int)
    if !ok {
        fmt.Println("sl is not a []int")
        return
    }
    fmt.Println(sl) // [1 2 3]
}

5.4 map 類型

使用 reflect 在運行時創建一個 map[string]int 類型的實例,並設置 map 實例中鍵值對:

func main() {
    typ := reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf(0))
    val := reflect.MakeMap(typ)
    key1 := reflect.ValueOf("one")
    value1 := reflect.ValueOf(1)
    key2 := reflect.ValueOf("two")
    value2 := reflect.ValueOf(2)
    val.SetMapIndex(key1, value1)
    val.SetMapIndex(key2, value2)
    fmt.Println(val.Interface()) // 輸出:map[one:1 two:2]

    m, ok := val.Interface().(map[string]int)
    if !ok {
        fmt.Println("m is not a map[string]int")
        return
    }

    fmt.Println(m)
}

5.5 channel 類型

使用 reflect 在運行時創建一個 chan int 類型的實例,並從該 channel 實例接收數據:

func main() {
    typ := reflect.ChanOf(reflect.BothDir, reflect.TypeOf(0))
    val := reflect.MakeChan(typ, 0)
    go func() {
        val.Send(reflect.ValueOf(42))
    }()

    ch, ok := val.Interface().(chan int)
    if !ok {
        fmt.Println("ch is not a chan int")
        return
    }
    fmt.Println(<-ch) // 42
}

5.6 結構體類型

使用 reflect 在運行時創建一個 struct 類型的實例,並設置該實例的字段值並調用該實例的方法:

type Person struct {
    Name string
    Age  int
}

func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s and I am %d years old\n", p.Name, p.Age)
}

func (p Person) SayHello(name string) {
    fmt.Printf("Hello, %s! My name is %s\n", name, p.Name)
}

func main() {
    typ := reflect.StructOf([]reflect.StructField{
        {
            Name: "Name",
            Type: reflect.TypeOf(""),
        },
        {
            Name: "Age",
            Type: reflect.TypeOf(0),
        },
    })
    ptrVal := reflect.New(typ)
    val := ptrVal.Elem()
    val.FieldByName("Name").SetString("Alice")
    val.FieldByName("Age").SetInt(25)

    person := (*Person)(ptrVal.UnsafePointer())
    person.Greet()         // 輸出:Hello, my name is Alice and I am 25 years old
    person.SayHello("Bob") // 輸出:Hello, Bob! My name is Alice
}

我們看到:上面代碼在反射世界中動態創建了一個帶有兩個字段 Name 和 Age 的 struct 類型,注意該 struct 類型與 Person 並非同一個類型,但他們的內存結構是一致的。這就是上面代碼尾部基於反射世界創建出的匿名 struct 顯式轉換爲 Person 類型後能正常工作的原因。

注:目前 reflect 不支持在運行時爲動態創建的結構體類型添加新方法。

5.7 指針類型

使用 reflect 在運行時創建一個指針類型的實例,並通過指針設置其指向內存對象的值:

type Person struct {
    Name string
    Age  int
}

func main() {
    typ := reflect.PtrTo(reflect.TypeOf(Person{}))
    val := reflect.New(typ.Elem())
    val.Elem().FieldByName("Name").SetString("Alice")
    val.Elem().FieldByName("Age").SetInt(25)
    person := val.Interface().(*Person)
    fmt.Println(person.Name) // 輸出:Alice
    fmt.Println(person.Age)  // 輸出:25
}
  1. 反射的使用場景

結合結構體標籤,Go 反射在實際開發中常用於以下兩個場景中:

這是我們最熟悉的場景。

反射機制可以用於將數據結構序列化成二進制或文本格式,或者將序列化後的數據反序列化成原始數據結構。比如標準庫的 encoding/json 包、xml 包、gob 包等就是使用反射機制實現的。

反射機制可以用於在 ORM(對象關係映射)中動態創建和修改對象,使得 ORM 能夠根據數據庫表結構自動創建對應的 Go 語言結構體。

注:我的 Go 語言精進之路 [10] 一書關於 Go 反射的講解中,有一個基於 Go 對象生成 sql 語句的例子。

當然 reflect 的應用不侷限在上述場景中,凡是需要在運行時瞭解類型信息、值信息的都可以嘗試使用 reflect 來實現,比如:編寫可以處理多種類型的通用函數 (可以用 interface{} 以及泛型替代)、利用通過 reflect.Type.Kind 的信息在代碼中做類型斷言、根據 reflect 得到的類型信息做代碼自動生成等。

下面是一個利用 reflect 手動解析 json 的示例,我們來看一下:

  1. 利用 reflect 手解 json 的例子

請注意:這不是一個可複用的完善的 json 解析代碼,僅僅是爲了演示而用。

例子代碼如下:

package main

import (
    "fmt"
    "reflect"
    "strings"
)

type Person struct {
    Name      string
    Age       int
    IsStudent bool
}

func main() {
    jsonStr := `{
        "name""John Doe",
        "age": 30,
        "isStudent"false
    }`

    person := Person{}
    parseJSONToStruct(jsonStr, &person)
    fmt.Printf("%+v\n", person)
}

func parseJSONToStruct(jsonStr string, v interface{}) {
    jsonLines := strings.Split(jsonStr, "\n")
    rv := reflect.ValueOf(v).Elem()

    for _, line := range jsonLines {
        line = strings.TrimSpace(line)
        if strings.HasPrefix(line, "{") || strings.HasPrefix(line, "}") {
            continue
        }

        parts := strings.SplitN(line, ":", 2)
        key := strings.TrimSpace(strings.Trim(parts[0]`"`))
        value := strings.TrimSpace(strings.Trim(parts[1], ","))

        // Find the corresponding field in the struct
        field := rv.FieldByNameFunc(func(fieldName string) bool {
            return strings.EqualFold(fieldName, key)
        })

        if field.IsValid() {
            switch field.Kind() {
            case reflect.String:
                field.SetString(strings.Trim(value, `"`))
            case reflect.Int:
                intValue, _ := strconv.Atoi(value)
                field.SetInt(int64(intValue))
            case reflect.Bool:
                boolValue := strings.ToLower(value) == "true"
                field.SetBool(boolValue)
            }
        }
    }
}

這段代碼不是很難理解。

parseJSONToStruct 函數首先將 JSON 字符串按行拆分,然後使用反射機制,獲取 v 所對應的結構體的值,並將其保存在 rv 變量中。

接下來,函數遍歷 JSON 字符串的每一行,如果該行以 {或} 開頭,則直接跳過。否則,將該行按冒號: 拆分成兩部分,一部分是鍵(key),一部分是值(value)。

然後,函數使用反射機制,查找結構體中與該鍵對應的字段。這裏使用了 FieldByNameFunc 方法,傳入一個匿名函數作爲參數,用於根據字段名查找對應的字段。如果找到了對應的字段,就根據該字段的類型,將值賦給該字段。這裏支持了三種類型的字段:字符串、整數和布爾值。

最終,函數會將解析後的結果保存在 v 中,由於 v 是一個空接口類型的變量,實際上保存的是對應結構體的值的指針。所以在函數外部使用 v 時,需要將其轉換爲對應的結構體類型。

  1. Go 反射的不足

Go 反射的優點在於它可以幫助我們實現更靈活和可擴展的程序設計。但是,Go 反射也存在一些缺陷和侷限性。其中,最主要的問題是性能。使用反射可能會導致程序性能下降,因爲反射需要進行類型檢查和動態分派,進出反射世界也需要額外的內存分配和裝箱和拆箱操作。在編寫高性能的 Go 程序時,應儘量避免使用反射機制。

此外,使用反射的代碼可讀性也相對較差,因爲反射代碼通常比較複雜和冗長。

  1. 小結

Go 反射是一種強大和靈活的機制,可以幫助我們實現運行時的類型和值信息獲取、值操作、方法 / 函數調用以及動態創建類型實例,本文涵蓋了所有這些操作的方法,希望能給大家帶去幫助。

本文中涉及的代碼可以在這裏 [11] 下載。

“Gopher 部落” 知識星球 [12] 旨在打造一個精品 Go 學習和進階社羣!高品質首發 Go 技術文章,“三天” 首發閱讀權,每年兩期 Go 語言發展現狀分析,每天提前 1 小時閱讀到新鮮的 Gopher 日報,網課、技術專欄、圖書內容前瞻,六小時內必答保證等滿足你關於 Go 語言生態的所有需求!2023 年,Gopher 部落將進一步聚焦於如何編寫雅、地道、可讀、可測試的 Go 代碼,關注代碼質量並深入理解 Go 核心技術,並繼續加強與星友的互動。歡迎大家加入!

Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯繫方式:

商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。

參考資料

[1] 

反射: https://en.wikipedia.org/wiki/Reflective_programming

[2] 

Go 語言精進之路: https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master

[3] 

Goroutine 調度: https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler

[4] 

Go 內存垃圾回收 (GC): https://t.zsxq.com/0eXDduygo

[5] 

《Go 語言第一課》: http://gk.link/a/10AVZ

[6] 

any: https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18

[7] 

Go 1.18: https://tonybai.com/2022/04/20/some-changes-in-go-1-18

[8] 

《使用反射操作 channel》: https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels

[9] 

《使用 reflect 包在反射世界裏讀寫各類型變量》: https://tonybai.com/2021/04/19/variable-operation-using-reflection-in-go

[10] 

Go 語言精進之路: https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master

[11] 

這裏: https://github.com/bigwhite/experiments/blob/master/reflect-guide

[12] 

“Gopher 部落” 知識星球: https://wx.zsxq.com/dweb2/index/group/51284458844544

[13] 

鏈接地址: https://m.do.co/c/bff6eed92687

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/J3hIbUaAp55l8QDuJYUQOg