深入理解 go reflect

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!
在这里插入图片描述

什么是反射

在计算机科学中,反射是指程序在运行时能够访问、检测和修改自身的结构和行为的能力。比如通过反射机制,可以动态地获取类的信息、创建对象、调用方法等。

在 Go 语言中,反射(Reflection)是一项强大但相对复杂的特性。它允许程序在运行时检查和操作变量的类型、值和结构。理解 Go 反射的原理对于编写灵活、可扩展和高级的 Go 程序至关重要,但是如果反射使用不当也会造成性能下降、可读性差等问题。

Go 反射的原理

类型与值

Go 语言是静态类型的语言,每一个变量都有一个静态类型,编译时就确定了具体的静态类型。

下面是官方博客里的一个例子:

type MyInt int

var i int
var j MyInt

变量i和j具有不同的静态类型,尽管它们具有相同的基础类型,但如果没有转换,它们就不能相互赋值。

值就是一种类型的一个实例。一个类型可以有许多不同的值,其中一个是它的零值。

在这里插入图片描述

反射的核心原理就是检查和操作变量的类型、值,Go 的反射通过 reflect.TypeOf() 、reflect.ValueOf() 来返回类型和值的相关信息,它们的返回值reflect.Type、reflect.Value 是实现反射的核心。

func TypeOf(i any) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func ValueOf(i any) Value {
    if i == nil {
       return Value{}
    }

    escapes(i)

    return unpackEface(i)
}

reflect.Type

reflect.Type 是一个接口,它代表了一个类型。我们可以通过 reflect.TypeOf 来获取一个类型的 reflect.Type 对象。 我们使用 reflect.Type 的目的通常是为了获取类型的信息,比如类型是什么、类型的名称、类型的字段、类型的方法等等。

type Type interface {
    // 所有的类型都可以调用下面这些函数

    // 此类型的变量对齐后所占用的字节数
    Align() int

    // 如果是 struct 的字段,对齐后占用的字节数
    FieldAlign() int

    // 返回类型方法集里的第 `i` (传入的参数)个方法
    Method(int) Method

    // 通过名称获取方法
    MethodByName(string) (Method, bool)

    // 获取类型方法集里导出的方法个数
    NumMethod() int

    // 类型名称
    Name() string

    // 返回类型所在的路径,如:encoding/base64
    PkgPath() string

    // 返回类型的大小,和 unsafe.Sizeof 功能类似
    Size() uintptr

    // 返回类型的字符串表示形式
    String() string

    // 返回类型的类型值
    Kind() Kind

    // 类型是否实现了接口 u
    Implements(u Type) bool

    // 是否可以赋值给 u
    AssignableTo(u Type) bool

    // 是否可以类型转换成 u
    ConvertibleTo(u Type) bool

    // 类型是否可以比较
    Comparable() bool

    // 下面这些函数只有特定类型可以调用
    // 如:Key, Elem 两个方法就只能是 Map 类型才能调用

    // 类型所占据的位数
    Bits() int

    // 返回通道的方向,只能是 chan 类型调用
    ChanDir() ChanDir

    // 返回类型是否是可变参数,只能是 func 类型调用
    // 比如 t 是类型 func(x int, y ... float64)
    // 那么 t.IsVariadic() == true
    IsVariadic() bool

    // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用
    Elem() Type

    // 返回结构体类型的第 i 个字段,只能是结构体类型调用
    // 如果 i 超过了总字段数,就会 panic
    Field(i int) StructField

    // 返回嵌套的结构体的字段
    FieldByIndex(index []int) StructField

    // 通过字段名称获取字段
    FieldByName(name string) (StructField, bool)

    // FieldByNameFunc returns the struct field with a name
    // 返回名称符合 func 函数的字段
    FieldByNameFunc(match func(string) bool) (StructField, bool)

    // 获取函数类型的第 i 个参数的类型
    In(i int) Type

    // 返回 map 的 key 类型,只能由类型 map 调用
    Key() Type

    // 返回 Array 的长度,只能由类型 Array 调用
    Len() int

    // 返回类型字段的数量,只能由类型 Struct 调用
    NumField() int

    // 返回函数类型的输入参数个数
    NumIn() int

    // 返回函数类型的返回值个数
    NumOut() int

    // 返回函数类型的第 i 个值的类型
    Out(i int) Type

    // 返回类型结构体的相同部分
    common() *rtype

    // 返回类型结构体的不同部分
    uncommon() *uncommonType
}

reflect.Type 是一个很大接口,但是其中某些方法只能作用于特定的类型:

reflect.Value

返回值 reflect.Value 是一个结构体,表示 interface{} 里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 *data 所指向的内容 —— 结构体的实际值。

反射三定律

反射可以将 interface 类型变量转换成反射对象。

反射对象是指通过反射机制返回的对象,例如:reflect.Type 或者 reflect.Value,在 Go 的反射过程中经常是先将静态类型转换接口类型,然后再将接口类型转换成反射对象:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

上面的代码实际上是经历了下面的转换过程:

对于为什么中间会有个 interface 类型,大家可能感到奇怪,原因是 reflect.TypeOf() 的函数签名中定义的参数类型是 interface 类型,在调用 reflect.TypeOf(x) 时就会把实参转换成 interface 类型,然后在reflect.TypeOf(x) 返回时就会将 interface 类型转换成反射对象。

反射可以将反射对象还原成 interface 对象。

反射是可逆的,既可以将 interface 对象转换成反射对象,又能将反射对象还原成接口对象:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

通过 Interface() 方法可以将反射对象还原成 interface 对象。

interface对象与反射对象之间的关系如下:

如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。

可设置性是反射对象的一个属性,其本质是反射对象有没有保存原始的对象。如果反射对象是由原始对象的拷贝而转换来的,那么反射对象是不可设置的:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet()) // settability of v: false

Go 的函数参数是值传递,所以调用 reflect.ValueOf 时对 x 进行了拷贝,v 是由 x 的副本转换来的,v 并没有保存原始对象 x 的相关信息,所以 v 是不可以被设置的。

如果想将 v 变成可设置的我们可以将原始对象设置成指针类型,但是仅使用指针是不够的,还需通过Elem 方法找到真正的对象:

var x float64 = 3.4
v := reflect.ValueOf(&x)
fmt.Println("settability of v:", v.CanSet()) // settability of v: false
fmt.Println("settability of v:", v.Elem().CanSet()) // settability of v: true

上面代码里 v 对应的 *x,v.Elem() 才是对应真正的 x:

这有点像通过指针改变原始对象的值:

func main() {
    var x float64 = 3.4
    var p *float64 = &x
    
    fmt.Println("x: ", x)
    // p = 3.14 // 错误
    (*p) = 3.14 // 正确
    fmt.Println("x: ", x)
}

$ go run main.go
x:  3.4
x:  3.14

Go 反射的应用示例

动态调用方法

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
}

func (p Person) SayHello() {
    fmt.Println("Hello, I'm " + p.Name)
}

func main() {
    p := Person{Name: "Alice"}
    v := reflect.ValueOf(p)
    method := v.MethodByName("SayHello")
    if method.IsValid() {
        method.Call(nil)
    }
}

动态创建对象

package main

import (
    "fmt"
    "reflect"
)

type Book struct {
    Title string
    Author string
}

func createObject(t reflect.Type) interface{} {
    v := reflect.New(t)
    return v.Interface()
}

func main() {
    t := reflect.TypeOf(Book{})
    book := createObject(t).(*Book)
    book.Title = "Effective Go"
    book.Author = "Someone"
    fmt.Println(book)
}

遍历和修改结构体字段

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name    string
    Age     int
    Grade   float64
}

func modifyStruct(s interface{}) {
    v := reflect.ValueOf(s)
    if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
        fmt.Println("输入不是结构体指针")
        return
    }
    v = v.Elem()
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        if field.CanSet() {
            switch field.Kind() {
            case reflect.String:
                field.SetString(field.String() + " Modified")
            case reflect.Int:
                field.SetInt(field.Int() + 10)
            case reflect.Float64:
                field.SetFloat(field.Float() + 10.0)
            }
        }
    }
}

func main() {
    student := Student{Name: "Tom", Age: 20, Grade: 85.5}
    modifyStruct(&student)
    fmt.Println(student)
}

反射的缺点

代码复杂性增加

type Person struct {
    Name string
}

func (p Person) SayHello() {}

type IPerson interface {
    SayHello()
}

func main() {
    // 通过反射调用
    p := Person{Name: "Alice"}
    v := reflect.ValueOf(p)
    method := v.MethodByName("SayHello")
    if method.IsValid() {
        method.Call(nil)
    }
    
    // 通过接口调用
    var ip IPerson
    ip = Person{Name: "Alice"}
    ip.SayHello()
    
    // 通过结构体调用
    p := Person{Name: "Alice"}
    p.SayHello()
}

我们可以非常直观的感受到反射的代码复杂度比较高。

性能问题

下面以方法调用为例:

type Person struct {
    Name string
}

func (p Person) SayHello() {

}

type IPerson interface {
    SayHello()
}

// 通过反射调用
func BenchmarkReflect(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       p := Person{Name: "Alice"}
       v := reflect.ValueOf(p)
       method := v.MethodByName("SayHello")
       if method.IsValid() {
          method.Call(nil)
       }
    }

}

// 接口方法调用
func BenchmarkInterface(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       var ip IPerson
       ip = Person{Name: "Alice"}
       ip.SayHello()
    }

}

// 结构体方法调用
func BenchmarkStruct(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       p := Person{Name: "Alice"}
       p.SayHello()
    }
}

>>> go test -bench=.
goos: darwin
goarch: amd64
pkg: go_test/reflect
BenchmarkReflect-12              5000000               306 ns/op             144 B/op          5 allocs/op
BenchmarkInterface-12           100000000               19.9 ns/op            16 B/op          1 allocs/op
BenchmarkStruct-12              2000000000               0.25 ns/op            0 B/op          0 allocs/op
PASS
ok      go_test/reflect 5.280s

可以看到通过反射调用方法是性能最差的,通过接口调用是反射的进20倍,结构体调用是反射的1000多倍。

如果有极致的性能要求就要避免反射和接口的使用,但是对大多数的程序来说,性能并不是惟一的指标,除了性能还需要关注可读性、扩展性等等,所以在实际开发中采用那种方式需要结合自身情况来考虑。

缺乏类型安全

由于反射是在运行时动态确定类型和操作,编译器无法在编译时进行类型检查,这可能导致运行时错误难以在开发阶段被发现。

var f float32 = 1.0
v := reflect.ValueOf(f)
// 报错:panic: reflect: call of reflect.Value.Int on float32 Value
fmt.Println(v.Int())

总结

Go 反射为开发者提供了在运行时动态操作数据结构和执行方法的能力,尽管它带来了极大的灵活性,但也应谨慎使用,因为不当的使用可能会导致代码可读性降低和性能开销增加。在实际应用中,应根据具体需求权衡是否使用反射,并确保其使用符合代码的整体设计和性能要求。

希望通过上述对 Go 反射原理的阐述和应用示例的展示,能够帮助您更深入地理解和运用这一强大的特性。

参考

https://blog.golang.org/laws-of-reflection

https://qcrao91.gitbook.io/go/fan-she

https://mp.weixin.qq.com/s/MuZYTSC6Q4YSYgmyAojzIw

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/#43-反射

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值