写点什么

深入探究 Golang 反射:功能与原理及应用

  • 2024-07-22
    福建
  • 本文字数:5446 字

    阅读完需:约 18 分钟

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
// 反射对象转换回interfacefunc (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 Valuesfunc (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")
// 函数内通过反射调用funcPtrfunc 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。步骤如下:

  • 通过 reflect.TypeOf 生成反射对象 reflect.Type

  • 通过 reflect.Type 获取 Field

  • 通过 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"}}
复制代码


文章转载自:golang架构师k哥

原文链接:https://www.cnblogs.com/killianxu/p/18314594

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
深入探究 Golang 反射:功能与原理及应用_golang_不在线第一只蜗牛_InfoQ写作社区