深入探究 Golang 反射:功能與原理及應用

golang架构师k哥發表於2024-07-21

Hi 親愛的朋友們,我是 k 哥。今天,咱們來一同探討下 Golang 反射。

Go 出於通用性的考量,提供了反射這一功能。藉助反射功能,我們可以實現通用性更強的函式,傳入任意的引數,在函式內透過反射動態呼叫引數物件的方法並訪問它的屬性。舉例來說,下面的bridge介面為了支援靈活呼叫任意函式,在執行時根據傳入引數funcPtr,透過反射動態呼叫funcPtr指向的具體函式。

func bridge(funcPtr interface{}, args ...interface{})

再如,ORM框架函式為了支援處理任意引數物件,在執行時根據傳入的引數,透過反射動態對引數物件賦值。

type User struct {
        Name string
        Age  int32
}
user := User{}
db.FindOne(&user)

本文將深入探討Golang反射包reflect的功能和原理。同時,我們學習某種東西,一方面是為了實踐運用,另一方面則是出於功利性面試的目的。所以,本文還會為大家介紹反射的典型應用以及高頻面試題。

1 關鍵功能

reflect包提供的功能比較多,但核心功能是把interface變數轉化為反射型別物件reflect.Type和reflect.Value,並透過反射型別物件提供的功能,訪問真實物件的方法和屬性。本文只介紹3個核心功能,其它方法可看官方文件。

1.物件型別轉換。透過TypeOf和ValueOf方法,可以將interface變數轉化為反射型別物件Type和Value。透過Interface方法,可以將Value轉換回interface變數。

type any = interface{}

// 獲取反射物件reflect.Type
// TypeOf returns the reflection Type that represents the dynamic type of i. 
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type

// 獲取反射物件reflect.Value
// ValueOf returns a new Value initialized to the concrete value stored in the interface i. 
// ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value

// 反射物件轉換回interface
func (v Value) Interface() (i any)

示例如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    age := 18
    fmt.Println("type: ", reflect.TypeOf(age)) // 輸出type:  int
    value := reflect.ValueOf(age)
    fmt.Println("value: ", value) // 輸出value:  18

    fmt.Println(value.Interface().(int)) // 輸出18
}

2.變數值設定。透過reflect.Value的SetXX相關方法,可以設定真實變數的值。reflect.Value是透過reflect.ValueOf(x)獲得的,只有當x是指標的時候,才可以透過reflec.Value修改實際變數x的值。

// Set assigns x to the value v. It panics if Value.CanSet returns false. 
// As in Go, x's value must be assignable to v's type and must not be derived from an unexported field.
func (v Value) Set(x Value)
func (v Value) SetInt(x int64)
...

// Elem returns the value that the interface v contains or that the pointer v points to. It panics if v's Kind is not Interface or Pointer. It returns the zero Value if v is nil.
func (v Value) Elem() Value

示例如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    age := 18
    // 透過reflect.ValueOf獲取age中的reflect.Value
    // 引數必須是指標才能修改其值
    pointerValue := reflect.ValueOf(&age)
    // Elem和Set方法結合,相當於給指標指向的變數賦值*p=值
    newValue := pointerValue.Elem()
    newValue.SetInt(28)
    fmt.Println(age) // 值被改變,輸出28

    // reflect.ValueOf引數不是指標
    pointerValue = reflect.ValueOf(age)
    newValue = pointerValue.Elem() // 如果非指標,直接panic: reflect: call of reflect.Value.Elem on int Value
}

3.方法呼叫。Method和MethodByName可以獲取到具體的方法,Call可以實現方法呼叫。

// Method returns a function value corresponding to v's i'th method. 
// The arguments to a Call on the returned function should not include a receiver; 
// the returned function will always use v as the receiver. Method panics if i is out of range or if v is a nil interface value.
func (v Value) Method(i int) Value

// MethodByName returns a function value corresponding to the method of v with the given name.
func (v Value) MethodByName(name string) Value

// Call calls the function v with the input arguments in. For example, if len(in) == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]). 
// Call panics if v's Kind is not Func. It returns the output results as Values
func (v Value) Call(in []Value) []Value

示例如下:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Age int
}

func (u User) ReflectCallFunc(name string) {
    fmt.Printf("age %d ,name %+v\n", u.Age, name)
}

func main() {
    user := User{18}

    // 1. 透過reflect.ValueOf(interface)來獲取到reflect.Value
    getValue := reflect.ValueOf(user)

    methodValue := getValue.MethodByName("ReflectCallFunc")
    args := []reflect.Value{reflect.ValueOf("k哥")}
    // 2. 透過Call呼叫方法
    methodValue.Call(args) // 輸出age 18 ,name k哥
}

2 原理

Go語言反射是建立在Go型別系統和interface設計之上的,因此在聊reflect包原理之前,不得不提及Go的型別和interface底層設計。

2.1 靜態型別和動態型別

在Go中,每個變數都會在編譯時確定一個靜態型別。所謂靜態型別(static type),就是變數宣告時候的型別。比如下面的變數i,靜態型別是interface

var i interface{}

所謂動態型別(concrete type,也叫具體型別),是指程式執行時系統才能看見的,變數的真實型別。比如下面的變數i,靜態型別是interface,但真實型別是int

var i interface{}   

i = 18 

2.2 interface底層設計

對於任意一個靜態型別是interface的變數,Go執行時都會儲存變數的值和動態型別。比如下面的變數age,會儲存值和動態型別(18, int)

var age interface{}
age = 18

2.3 reflect原理

reflect是基於interface實現的。透過interface底層資料結構的動態型別和資料,構造反射物件。

reflect.TypeOf獲取interface底層的動態型別,從而構造出reflect.Type物件。透過Type,可以獲取變數包含的方法、欄位等資訊。

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i)) // eface為interface底層結構
    return toType(eface.typ) // eface.typ就是interface底層的動態型別
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

reflect.ValueOf獲取interface底層的Type和資料,封裝成reflect.Value物件。

type Value struct {
    // typ holds the type of the value represented by a Value.
    typ *rtype // 動態型別

    // Pointer-valued data or, if flagIndir is set, pointer to data.
    // Valid when either flagIndir is set or typ.pointers() is true.
    ptr unsafe.Pointer // 資料指標

    // flag holds metadata about the value.
    // The lowest bits are flag bits:
    //  - flagStickyRO: obtained via unexported not embedded field, so read-only
    //  - flagEmbedRO: obtained via unexported embedded field, so read-only
    //  - flagIndir: val holds a pointer to the data
    //  - flagAddr: v.CanAddr is true (implies flagIndir)
    //  - flagMethod: v is a method value.
    // The next five bits give the Kind of the value.
    // This repeats typ.Kind() except for method values.
    // The remaining 23+ bits give a method number for method values.
    // If flag.kind() != Func, code can assume that flagMethod is unset.
    // If ifaceIndir(typ), code can assume that flagIndir is set.
    flag // 標記位,用於標記此value是否是方法、是否是指標等

}

type flag uintptr

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }
    return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
    // interface底層結構
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    // 動態型別
    t := e.typ
    if t == nil {
        return Value{}
    }
    // 標記位,用於標記此value是否是方法、是否是指標等
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f} // t為型別,e.word為資料,
}

3 應用

工作中,反射常見應用場景有以下兩種:

1.不知道介面呼叫哪個函式,根據傳入引數在執行時透過反射呼叫。例如以下這種橋接模式:

package main

import (
    "fmt"
    "reflect"
)

// 函式內透過反射呼叫funcPtr
func bridge(funcPtr interface{}, args ...interface{}) {
    n := len(args)
    inValue := make([]reflect.Value, n)
    for i := 0; i < n; i++ {
        inValue[i] = reflect.ValueOf(args[i])
    }
    function := reflect.ValueOf(funcPtr)
    function.Call(inValue)
}

func call1(v1 int, v2 int) {
    fmt.Println(v1, v2)
}
func call2(v1 int, v2 int, s string) {
    fmt.Println(v1, v2, s)
}
func main() {
    bridge(call1, 1, 2)         // 輸出1 2
    bridge(call2, 1, 2, "test") // 輸出1 2 test
}

2.不知道傳入函式的引數型別,函式需要在執行時處理任意引數物件,這種需要對結構體物件反射。典型應用場景是ORM,orm示例如下:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int32
}

func FindOne(x interface{}) {
    sv := reflect.ValueOf(x)
    sv = sv.Elem()
    // 對於orm,改成從db裡查出來再透過反射設定進去
    sv.FieldByName("Name").SetString("k哥")
    sv.FieldByName("Age").SetInt(18)
}

func main() {
    user := &User{}
    FindOne(user)
    fmt.Println(*user) // 輸出 {k哥 18}
}

4 高頻面試題

1.reflect(反射包)如何獲取欄位 tag?

透過反射包獲取tag。步驟如下:

  1. 透過reflect.TypeOf生成反射物件reflect.Type

  2. 透過reflect.Type獲取Field

  3. 透過Field訪問tag

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name" otherTag:"name"`
    age  string `json:"age"`
}

func main() {
    user := User{}
    userType := reflect.TypeOf(user)
    field := userType.Field(0)
    fmt.Println(field.Tag.Get("json"), field.Tag.Get("otherTag")) // 輸出name name

    field = userType.Field(1)
    fmt.Println(field.Tag.Get("json")) // 輸出age
}


2.為什麼 json 包不能匯出私有變數的 tag?

從1中的例子中可知,反射可以訪問私有變數age的tag。json包之所以不能匯出私有變數,是因為json包的實現,將私有變數的tag跳過了。

func typeFields(t reflect.Type) structFields {
    // Scan f.typ for fields to include.
    for i := 0; i < f.typ.NumField(); i++ {
        sf := f.typ.Field(i)
        // 非匯出成員(私有變數),忽略tag
        if !sf.IsExported() {
            // Ignore unexported non-embedded fields.
            continue
        }
        tag := sf.Tag.Get("json")
        if tag == "-" {
            continue
        }          
    }
}

3.json包裡使用的時候,結構體裡的變數不加tag能不能正常轉成json裡的欄位?

  1. 如果是私有成員,不能轉,因為json包會忽略私有成員的tag資訊。比如下面的demo中,User結構體中的a和b都不能json序列化。

  2. 如果是公有成員。

  • 不加tag,可以正常轉為json裡的欄位,json的key跟結構體內欄位名一致。比如下面的demo,User中的C序列化後,key和結構體欄位名保持一致是C。
  • 加了tag,從struct轉json的時候,json的key跟tag的值一致。比如下面的demo,User中的D序列化後是d。
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    a string // 小寫無tag
    b string `json:"b"` //小寫+tag
    C string //大寫無tag
    D string `json:"d"` //大寫+tag
}

func main() {
    u := User{
        a: "1",
        b: "2",
        C: "3",
        D: "4",
    }
    fmt.Printf("%+v\n", u) // 輸出{a:1 b:2 C:3 D:4}
    jsonInfo, _ := json.Marshal(u)
    fmt.Printf("%+v\n", string(jsonInfo)) // 輸出{"C":"3","d":"4"}
}

相關文章