Go 語言反射編程指南
反射 [1] 是一種編程語言的高級特性,它允許程序在運行時檢視自身的結構和行爲。通過反射,程序可以動態地獲取類型 (type) 與值 (value) 等信息,並對它們進行操作,諸如修改字段、調用方法等,這使得程序具有更大的靈活性和可擴展性。
不過,反射雖然具有強大的功能,但也存在一些缺點。由於反射是在運行時進行的,因此它比直接調用代碼的性能要差。此外,反射還可能導致代碼的可讀性和維護性降低,因爲它使得程序行爲更加難以預測和理解。因此,在使用反射時需要注意性能和可維護性。
Go 從誕生伊始就在運行時支持了反射,並在標準庫中提供了 reflect 包供開發者進行反射編程時使用。在這篇文章中,我們就來系統地瞭解一下如何在 Go 中通過 reflect 包實現反射編程。
- 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 函數的基本用法的示例,下面我們再來詳細看看獲取不同類型的類型信息和值信息的細節。
- 檢視類型信息和調用類型方法
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 純粹爲了演示,不要計較其合理性問題。
- 獲取與修改值信息
掌握瞭如何在反射世界獲取一個變量的類型信息後,我們再來看看如何在反射世界獲取並修改一個變量的值信息。之前在《使用 reflect 包在反射世界裏讀寫各類型變量》[9] 一文中詳細講解了使用 reflect 讀寫變量的值信息,大家可以移步那篇文章閱讀。
注:並不是所有變量都可以修改值的,可以使用 Value 的 CanSet 方法判斷值是否可以設置。
- 調用函數與方法
通過反射我們可以在反射世界調用函數,也可以調用特定類型的變量的方法。
下面是一個通過 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 目前不支持對非導出方法的調用。
- 動態創建類型實例
reflect 更爲強大的功能是可以在運行時動態創建各種類型的實例。下面是在反射世界動態創建各種類型實例的示例。
5.1 基本類型
下面以 int、float64 和 string 爲例演示一下如何通過 reflect 在運行時動態創建基本類型的實例。
- 創建 int 類型實例
func main() {
val := reflect.New(reflect.TypeOf(0))
val.Elem().SetInt(42)
fmt.Println(val.Elem().Int()) // 輸出:42
}
- 創建 float64 類型實例
func main() {
val := reflect.New(reflect.TypeOf(0.0))
val.Elem().SetFloat(3.14)
fmt.Println(val.Elem().Float()) // 輸出:3.14
}
- 創建 string 類型實例
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
}
- 反射的使用場景
結合結構體標籤,Go 反射在實際開發中常用於以下兩個場景中:
- 序列化和反序列化
這是我們最熟悉的場景。
反射機制可以用於將數據結構序列化成二進制或文本格式,或者將序列化後的數據反序列化成原始數據結構。比如標準庫的 encoding/json 包、xml 包、gob 包等就是使用反射機制實現的。
- 實現 ORM 框架
反射機制可以用於在 ORM(對象關係映射)中動態創建和修改對象,使得 ORM 能夠根據數據庫表結構自動創建對應的 Go 語言結構體。
注:我的 Go 語言精進之路 [10] 一書關於 Go 反射的講解中,有一個基於 Go 對象生成 sql 語句的例子。
當然 reflect 的應用不侷限在上述場景中,凡是需要在運行時瞭解類型信息、值信息的都可以嘗試使用 reflect 來實現,比如:編寫可以處理多種類型的通用函數 (可以用 interface{} 以及泛型替代)、利用通過 reflect.Type.Kind 的信息在代碼中做類型斷言、根據 reflect 得到的類型信息做代碼自動生成等。
下面是一個利用 reflect 手動解析 json 的示例,我們來看一下:
- 利用 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 時,需要將其轉換爲對應的結構體類型。
- Go 反射的不足
Go 反射的優點在於它可以幫助我們實現更靈活和可擴展的程序設計。但是,Go 反射也存在一些缺陷和侷限性。其中,最主要的問題是性能。使用反射可能會導致程序性能下降,因爲反射需要進行類型檢查和動態分派,進出反射世界也需要額外的內存分配和裝箱和拆箱操作。在編寫高性能的 Go 程序時,應儘量避免使用反射機制。
此外,使用反射的代碼可讀性也相對較差,因爲反射代碼通常比較複雜和冗長。
- 小結
Go 反射是一種強大和靈活的機制,可以幫助我們實現運行時的類型和值信息獲取、值操作、方法 / 函數調用以及動態創建類型實例,本文涵蓋了所有這些操作的方法,希望能給大家帶去幫助。
本文中涉及的代碼可以在這裏 [11] 下載。
“Gopher 部落” 知識星球 [12] 旨在打造一個精品 Go 學習和進階社羣!高品質首發 Go 技術文章,“三天” 首發閱讀權,每年兩期 Go 語言發展現狀分析,每天提前 1 小時閱讀到新鮮的 Gopher 日報,網課、技術專欄、圖書內容前瞻,六小時內必答保證等滿足你關於 Go 語言生態的所有需求!2023 年,Gopher 部落將進一步聚焦於如何編寫雅、地道、可讀、可測試的 Go 代碼,關注代碼質量並深入理解 Go 核心技術,並繼續加強與星友的互動。歡迎大家加入!
Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily
我的聯繫方式:
-
微博 (暫不可用):https://weibo.com/bigwhite20xx
-
微博 2:https://weibo.com/u/6484441286
-
博客:tonybai.com
-
github: https://github.com/bigwhite
商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。
參考資料
[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