Go 反射包詳解
當我們接觸一個新名詞的時候,我們需要做的事情就是知道其定義,然後盡快適應,反射就是這樣的名詞之一。
首先是定義:反射允許程序在運行時檢查和操作其結構、變量、方法等信息。Go 語言提供了反射包(reflect),使得我們能夠在運行時動態地獲取類型信息、操作對象的字段和方法。
你現在知道定義了,接下來就需要適應這個新名詞出現在自己的世界裡,然後盡快適應它。
如何盡快適應呢?就是通過大量練習,盡可能讓自己在實踐中使用到。
ps: 本文最後的反射示例是我們公司內部使用框架的一個小 demo,可以通過方法名自動註冊 HTTP 方法。
正文開始:
反射怎麼用#
在 Go 語言中每個字段都有:
- 類型
- 值
比如
A := 10 // a 的類型就是int,值就是10,這是go語言的語法糖幫助我們簡單創建的類型
var a int = 10 // 實際上應該是這樣創建, 在聲明值的時候標註類型
Reflect 的兩個主要類型是reflect.Type
和reflect.Value
,可以讓我們獲取任意對象的 Type 和 Value。
Type#
當我們想要獲取一個變量的類型信息時,可以使用 reflect.TypeOf 函數來實現。下面是一個使用 reflect.TypeOf 的示例:
package main
import (
"fmt"
"reflect"
)
func main() {
var num int = 42
var str string = "Hello, Reflect!"
fmt.Println(reflect.TypeOf(num)) // 輸出: int
fmt.Println(reflect.TypeOf(str)) // 輸出: string
}
實際上 reflect 不僅僅只有 Type 還有一個 Kind。Kind 你可以理解為字段的底層類型。比如下面這個例子
var a myFloat64 // 自定義類型
var b *float64 // 指針
reflectType(a) // 輸出:TypeOf: main.myFloat64, 類型:myFloat64,種類:float64
reflectType(b) // 輸出:TypeOf: TypeOf: *float64, 類型:,種類:ptr
type Person struct {
Name string
Age int
}
var p = Person{
Name: "Lixin",
Age: 21,
}
reflectType(p) // TypeOf: TypeOf: main.Person, 類型:Person,種類:struct
你可以注意到,像 slice,map,指針等變量的.Name
返回都是空,它們在 Go 語言中都被視為類型的底層實現或衍生類型,而不是具體的命名類型。
對於數組和切片類型,它們的名稱在 Go 語言中是以表達式形式表示的,例如 [5] int 表示長度為 5 的整型數組,[] string 表示字符串切片。由於這些類型是通過表達式定義的,而不是具體的命名類型,所以它們的.Name () 方法返回空。
對於 map 類型,它的名稱是 map,而不包含具體的鍵類型和值類型信息。因為映射類型在 Go 語言中是一種內置類型,並且可以使用不同的鍵類型和值類型進行實例化,所以它的.Name () 方法返回空。
對於指針類型,它的名稱是加上指針指向的類型的名稱。例如,對於int 類型的指針變量,它的名稱是 * int。然而,由於指針類型本身沒有具體的命名,只是指向其他類型的引用,所以它的.Name () 方法返回空。
這樣設計的目的是為了遵循 Go 語言的類型系統和語法約定。通過反射,我們可以使用.Kind () 方法獲取這些類型的種類信息,以便進一步判斷和處理。
在這裡,你可以可以進一步在 reflect 包中得知 go 的真正的 Kind 的定義
// go 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
)
Value#
Reflect 的 Value 類型就是 Go 中每個字段的值信息,我們可以通過ValueOf()
來獲得原始值信息。
以下是其中一些常用的方法:
- Interface ():返回一個 interface {} 類型的值,表示 reflect.Value 持有的原始值。可以通過類型斷言將其轉換為具體類型。
- Bool ():返回 bool 類型的原始值。
- Int ():返回 int 類型的原始值。
- Float ():返回 float64 類型的原始值。
- String ():返回 string 類型的原始值。
- Type ():返回 reflect.Type 類型,表示 reflect.Value 持有的值的類型。
下面這個例子,你就應該明白怎麼用了:
package main
import (
"fmt"
"reflect"
)
func main() {
// 使用 reflect.ValueOf 獲取 reflect.Value
v := reflect.ValueOf(42)
// 獲取原始值
value := v.Interface()
fmt.Println(value) // 輸出: 42
// 使用類型斷言將原始值轉換為具體類型
if i, ok := value.(int); ok {
fmt.Println(i * 2) // 輸出: 84
}
// 使用其他方法獲取原始值
b := reflect.ValueOf(true).Bool()
fmt.Println(b) // 輸出: true
f := reflect.ValueOf(3.14).Float()
fmt.Println(f) // 輸出: 3.14
s := reflect.ValueOf("hello").String()
fmt.Println(s) // 輸出: hello
t := reflect.ValueOf(42).Type()
fmt.Println(t) // 輸出: int
}
通過反射來更改變量的底層值#
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 25}
// 使用反射獲取變量的指針值
// 這裡必須是指針,因為我們是通過底層指針去更改值的
v := reflect.ValueOf(&p)
// 檢查是否是指針類型並且可尋址
if v.Kind() == reflect.Ptr && v.Elem().CanSet() {
// 獲取指針指向的值
elem := v.Elem()
// 獲取字段的值並修改
nameField := elem.FieldByName("Name")
if nameField.IsValid() && nameField.Kind() == reflect.String {
nameField.SetString("Bob")
}
ageField := elem.FieldByName("Age")
if ageField.IsValid() && ageField.Kind() == reflect.Int {
ageField.SetInt(30)
}
}
fmt.Printf("%+v", p) // 輸出: {Name:Bob Age:30}
}
isValid#
你可能注意到了我們上個例子中有個沒有提到的方法IsValid()
func (v Value) IsValid() bool
返回 v 是否持有一個值。如果 v 是 Value 零值會返回假,此時 v 除了 IsValid、String、Kind 之外的方法都會導致 panic。
func (v Value) IsNil() bool
IsNil () 報告 v 持有的值是否為 nil。v 持有的值的分類必須是通道、函數、接口、映射、指針、切片之一;否則 IsNil 函數會導致 panic。
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
// *int類型空指針
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) //var a *int IsNil: true
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // nil IsValid: false
// 實例化一個匿名構造體
b := struct{}{}
// 嘗試從構造體中查找"abc"字段
fmt.Println("不存在的構造體成員:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 不存在的構造體成員: false
// 嘗試從構造體中查找"abc"方法
fmt.Println("不存在的構造體方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // 不存在的構造體方法: false
// map
c := map[string]int{}
// 嘗試從map中查找一個不存在的鍵
fmt.Println("map中不存在的鍵:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) // map中不存在的鍵: false
}
結構體反射#
當我們從 reflect 獲得類型信息後,如果類型是結構體的話,那我們可以將通過 NumField () 和 Field () 方法獲得結構體成員的詳細信息,甚至通過類型的實例來獲得類型的方法。
比如下面這個例子:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
Height float64
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{
Name: "John",
Age: 30,
Height: 1.75,
}
// 獲取結構體字段信息
t := reflect.TypeOf(p)
fmt.Println("Struct fields:")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Name: %s, Type: %s\n", field.Name, field.Type)
}
// 根據字段名獲取結構體字段信息
field, ok := t.FieldByName("Age")
if ok {
fmt.Println("\nField by name:")
fmt.Printf("Name: %s, Type: %s\n", field.Name, field.Type)
}
// 獲取結構體方法信息
v := reflect.ValueOf(p)
fmt.Println("\nMethods:")
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
fmt.Printf("Name: %s, Type: %s\n", method.Name, method.Type)
}
// 根據方法名獲取結構體方法信息
method, ok := t.MethodByName("SayHello")
if ok {
fmt.Println("\nMethod by name:")
fmt.Printf("Name: %s, Type: %s\n", method.Name, method.Type)
}
}
// 輸出
Struct fields:
Name: Name, Type: string
Name: Age, Type: int
Name: Height, Type: float64
Field by name:
Name: Age, Type: int
Methods:
Name: SayHello, Type: func(main.Person)
Method by name:
Name: SayHello, Type: func(main.Person)
通過上面這個例子,你可以看出 reflect 的威力,比如我可以通過某個結構體的方法,只要將方法名稱寫成一定的格式,就可以幫我自動註冊 HTTP 方法,比如我有個結構體 Service,只要在這個結構體下寫 Get_XXX, 就可以註冊路徑為 /xxx 的 get 服務,下面就是一個示例:
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
)
type Service struct{}
func (s *Service) Get_Hubs(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handling GET request for /hubs")
}
func (s *Service) Post_Name(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handling POST request for /name")
}
func RegisterHTTPHandlers(service interface{}) {
svcType := reflect.TypeOf(service)
svcValue := reflect.ValueOf(service)
for i := 0; i < svcType.NumMethod(); i++ {
method := svcType.Method(i)
methodName := method.Name
if strings.HasPrefix(methodName, "Get_") || strings.HasPrefix(methodName, "Post_") {
parts := strings.Split(methodName, "_")
if len(parts) < 2 {
continue
}
path := "/" + strings.ToLower(strings.Join(parts[1:], "/"))
handler := svcValue.MethodByName(methodName).Interface().(func(http.ResponseWriter, *http.Request))
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if strings.HasPrefix(methodName, "Get_") {
handler(w, r)
} else {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
case http.MethodPost:
if strings.HasPrefix(methodName, "Post_") {
handler(w, r)
} else {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})
}
}
}
func main() {
service := &Service{}
RegisterHTTPHandlers(service)
fmt.Println("Listening on http://localhost:8889")
http.ListenAndServe(":8889", nil)
}
在這個示例中,我們定義了一個 Service 結構體,其中包含了 Get_Hubs () 和 Post_Name () 兩個方法。然後,我們編寫了 RegisterHTTPHandlers () 函數來根據方法的名稱自動註冊 HTTP 處理程序。
然後這個程序就註冊了get hubs
和 post name
的方法,你可以使用curl
或者postman
工具來不斷向 8889 分別發送對應的請求,會發現終端打印了這些。
Listening on http://localhost:8889
Handling POST request for /name
Handling GET request for /hubs
再接著繼續想想,既然可以註冊 Get 和 Post 請求方法,那是不是我們還可以進一步操作比如動態綁定參數等等。通過反射,我們都可以做到。
總結#
該文章介紹了 Go 語言中的反射包(reflect)的使用。反射允許程序在運行時檢查和操作其結構、變量、方法等信息。通過反射,可以動態地獲取類型信息、操作對象的字段和方法。
我們首先解釋了反射的定義和作用,並提到了反射包中的兩個主要類型:reflect.Type
和reflect.Value
。reflect.TypeOf
函數用於獲取變量的類型信息,而reflect.ValueOf
函數用於獲取變量的值信息。
介紹了如何使用反射來獲取變量的類型信息和值信息。通過示例代碼展示了如何使用reflect.TypeOf
和reflect.ValueOf
函數,並介紹了reflect.Type
和reflect.Value
類型的常用方法,如Interface()
、Bool()
、Int()
、Float()
和String()
等。
還提到了IsValid()
和IsNil()
方法的使用,用於判斷反射值是否有效或為 nil。
然後介紹了如何使用反射來更改變量的底層值。通過示例代碼演示了如何通過反射獲取變量的指針值,並使用SetString()
和SetInt()
等方法修改結構體的字段值。
接著,我們詳細介紹了結構體反射。通過示例代碼展示了如何使用反射獲取結構體的字段信息和方法信息,包括使用NumField()
、Field()
、Method()
和MethodByName()
等方法。
最後,我給出了一個應用示例,通過反射自動註冊 HTTP 方法的功能。通過解析結構體的方法名稱,將滿足特定命名格式的方法自動註冊為相應的 HTTP 服務。
總的來說,該文章詳細介紹了 Go 語言中反射包的使用方法,包括獲取類型信息、值信息,修改變量值以及結構體的反射等操作。反射是一項強大的功能,可以在某些場景下提供靈活性和便利性。但需要注意,由於反射使用了運行時信息,會帶來一定的性能損耗,因此在性能敏感的場景中需要謹慎使用。