深入理解Golang的reflect原理

2023-05-05 11:05:47 浏览数 (1)

TOC

1. 什么是reflect?

反射是指在运行期对程序本身进行访问和修改的能力。程序编译后,变量被转换为内存地址,而变量名无法被编译器写入可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在编译器将变量的反射信息如字段名称、类型信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样可以在程序运行期获取类型的反射信息, 并修改他们。

反射:反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力

Go使用reflect包访问程序的反射信息。

  • 支持反射的语言:Go、Java、C#,而C/C 没有反射功能。
  • Lua,JavaScript动态语言,可以在运行期访问程序自身的值与类型,故不需要反射特性

Go提供了一种在运行时更新和检查变量的值、调用变量的方法的机制,但在编译器不知道这些变量的具体类型,这种机制被称为反射

2. reflect 使用场景

示例1:

代码语言:go复制
type student struct {
    name  string
    age   uint8
    infos interface{}
}

func TestReflect(t *testing.T) {
    s := &student{
        name: "zhangSan",
        age:  18,
        infos: map[string]interface{}{
            "class": "class1",
            "grade": uint8(1),
            "read": func(str string) {
                fmt.Println(str)
            },
        },
    }
    options := s.infos
    fmt.Println("infos type:", reflect.TypeOf(options))
    fmt.Println("infos value:", reflect.ValueOf(options))

    fmt.Println("infos.class type:", reflect.TypeOf(options.(map[string]interface{})["class"]))
    fmt.Println("infos.class value:", reflect.ValueOf(options.(map[string]interface{})["class"]))

    fmt.Println("infos.grade type:", reflect.TypeOf(options.(map[string]interface{})["grade"]))
    fmt.Println("infos.grade value:", reflect.ValueOf(options.(map[string]interface{})["grade"]))

    fmt.Println("infos.read type:", reflect.TypeOf(options.(map[string]interface{})["read"]))
    fmt.Println("infos.read value:", reflect.ValueOf(options.(map[string]interface{})["read"]))

    read := options.(map[string]interface{})["read"]
    if reflect.TypeOf(read).Kind() == reflect.Func {
        read.(func(str string))("I am reading!")
    }
}

结果:

=== RUN   TestReflect
infos type: map[string]interface {}
infos value: map[class:class1 grade:1 read:0x10ef520]
infos.class type: string
infos.class value: class1
infos.grade type: uint8
infos.grade value: 1
infos.read type: func(string)
infos.read value: 0x10ef520
I am reading!
--- PASS: TestReflect (0.00s)
PASS

示例2:

代码语言:go复制
type Student struct {
    Name string `json:"name1" db:"name2"`
    Age  int    `json:"age1" db:"age2"`
}

func main() {
    var s Student
    v := reflect.ValueOf(&s)
    // 类型
    t := v.Type()
    // 获取字段
    for i := 0; i < t.Elem().NumField(); i   {
        f := t.Elem().Field(i)
        fmt.Println(f.Tag.Get("json"))
        fmt.Println(f.Tag.Get("db"))
    }
}


结果:
name1
name2
age1
age2

从上述的例子中,我们可以看出golang提供了一种数据类型interface, 一种可以自定义的数据类型。通过reflect就可以反射出自定义的类型

3. reflect 实现原理

从上一个章节中就可以看出,要想弄明白在Golang中是如何实现反射的,那么就需要先了解什么是interface ?

3.1 反射的基础: interface{}

interface{} 存储结构

go 的接口是由两部分组成的,一部分是类型信息,另一部分是数据信息。

代码语言:go复制
var a = 1
var b interface{} = a

对于这个例子,b 的类型信息是 int,数据信息是 1,这两部分信息都是存储在 b 里面的。b 的内存结构如下:

在上图中,b 的类型实际上是 eface,它是一个空接口,在src/runtime/runtime2.go 中,它的定义如下:

代码语言:go复制
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

也就是说,一个 interface{} 中实际上既包含了变量的类型信息,也包含了类型的数据。 正因为如此,我们才可以通过反射来获取到变量的类型信息,以及变量的数据信息。

3.2 反射对象 reflect.Type & reflect.Value

  • reflect.TypeOf :返回反射类型(returns the reflection Type that represents the dynamic type of i)
  • reflect.ValueOf:返回反射值(returns a new Value initialized to the concrete value)

反射可以将接口类型变量转换为反射类型对象

代码语言:go复制
var a = 1
t := reflect.TypeOf(a)  // t = int

var b = "hello"
v := reflect.ValueOf(b)  // v = "hello"

reflect.TypeOf() 源码:

代码语言:go复制
func TypeOf(i any) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

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

reflect.ValueOf() 源码:

代码语言:go复制
func ValueOf(i any) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// Dummy annotation marking that the value x escapes,
// for use in cases where the reflect code is so clever that
// the compiler cannot follow.
func escapes(x any) {
    if dummy.b {
        dummy.x = x
    }
}

var dummy struct {
    b bool
    x any
}

我们去看一下 TypeOfValueOf 的源码会发现,这两个方法都接收一个 interface{} 类型的参数,然后返回一个 reflect.Typereflect.Value 类型的值。这也就是为什么我们可以通过 reflect.TypeOfreflect.ValueOf 来获取到一个变量的类型和值的原因。

3.3 反射定律

在 go 官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:

  1. 反射可以将 interface 类型变量转换成反射对象。
  2. 反射可以将反射对象还原成 interface 对象。
  3. 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。

关于这三条定律,官方博客已经有了比较完整的阐述,感兴趣的可以去看一下官方博客的文章。这里简单阐述一下:

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

上文中举的例子中已经有了很清楚的表示,就不再举例了。

2. 反射可以将反射对象还原成 interface 对象

我们可以通过 reflect.Value.Interface 来获取到反射对象的 interface 对象,也就是传递给 reflect.ValueOf 的那个变量本身。 不过返回值类型是 interface{},所以我们需要进行类型断言。

代码语言:go复制
type Student struct {
    Name string `json:"name1" db:"name2"`
    Age  int    `json:"age1" db:"age2"`
}

func main() {
    var s Student
    v := reflect.ValueOf(&s)

    // 将反射对象还原成interface对象
    i := v.Interface()
    fmt.Println(i.(*Student))
}

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

我们可以通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过 reflect.Value.Set 来修改反射对象的值。 这其实也是非常场景的使用反射的一个场景,通过反射来修改变量的值。

代码语言:go复制
func main() {
    s := &Student{
        Name: "zhangSan",
        Age:  18,
    }
    v := reflect.ValueOf(s)

    fmt.Println("set ability of v:", v.CanSet())           // false
    fmt.Println("set ability of Elem:", v.Elem().CanSet()) // true
}

可设置要求:

  1. 反射对象是一个指针
  2. 这个指针指向的是一个可设置的变量

原因:

如果这个值只是一个普通的变量,这个值实际上被拷贝了一份。如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。 所以 go 语言在这里做了一个限制。

为什么v.CanSet() == false

v 是一个指针,而 v.Elem() 是指针指向的值,对于这个指针本身,我们修改它是没有意义的,我们可以设想一下,

如果我们修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样我们的指针变量就不是指向 x 了,

而是指向了其他的变量,这样就不符合我们的预期了。所以 v.CanSet() 返回的是 false

示例

代码语言:go复制
type Student struct {
    Name string `json:"name1" db:"name2"`
    Age  int    `json:"age1" db:"age2"`
}

func main() {
    s := &Student{
        Name: "zhangSan",
        Age:  18,
    }
    v := reflect.ValueOf(s)

    fmt.Println("set ability of v:", v.CanSet())           // false
    fmt.Println("set ability of Elem:", v.Elem().CanSet()) // true

    if v.Elem().CanSet() {
        for i := 0; i < v.Elem().NumField(); i   {
            switch v.Elem().Field(i).Kind() {
            case reflect.String:
                v.Elem().Field(i).Set(reflect.ValueOf("lisi"))
            case reflect.Int:
                v.Elem().Field(i).Set(reflect.ValueOf(20))
            }
        }
    }

    fmt.Println("v: ", v)
    fmt.Println("student: ", v.Interface().(*Student))
}

4. 常用的方法

4.1 Elem

从上文中可以看出Elem,是element的缩写,可以获取reflect对象中的每一个子元素。但是reflect.Type的Elemreflect.Value 的 Elem 还是有所区别的。

reflect.Type 的 Elem 方法

reflect.TypeElem 方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于 reflect.Type 来说,

能调用 Elem 方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的 reflect.Type 调用 Elem 方法会 panic

获取 map 类型 key 的类型信息,需要使用 Key 方法,而不是 Elem 方法。

代码语言:go复制
s2 := map[string]interface{}{
    "name": "zhangSan",
    "age":  18,
}

s3 := map[interface{}]interface{}{
    "name": "zhangSan",
    "age":  18,
}

fmt.Println(reflect.TypeOf(s2).Key())  // string
fmt.Println(reflect.TypeOf(s3).Key())  // interface {}

reflect.Value 的 Elem 方法

reflect.ValueElem 方法的作用是获取指针指向的值,或者获取接口的动态值

上文中,修改反射对象章节中,有具体例子。

4.2 Interface 方法

获取反射对象的动态值。 也就是说,如果反射对象是一个指针,那么 Interface 方法会返回指针指向的值。

代码语言:go复制
type Student struct {
    Name string `json:"name1" db:"name2"`
    Age  int    `json:"age1" db:"age2"`
}

func main() {
    var s Student
    v := reflect.ValueOf(&s)

    // 将反射对象还原成interface对象
    i := v.Interface()
    fmt.Println(i.(*Student))
}

4.3 Kind 方法

Kind 表示的是 go 底层类型系统中的类型。

我们定义的类型在 go 的类型系统中都是基本类型的一种,这个基本类型就是 Kind

也正因为如此,我们可以通过有限的 reflect.TypeKind来进行类型判断。

代码语言:go复制
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
)
代码语言:go复制
type Student struct {
    Name string
    Age  uint8
    Say  func(sth string)
}

func TestReflect(t *testing.T) {
    s := &Student{
        Name: "zhangSan",
        Age:  18,
        Say: func(sth string) {
            fmt.Println(sth)
        },
    }

    v := reflect.ValueOf(s)

    for i := 0; i < v.Elem().NumField(); i   {
        switch v.Elem().Field(i).Kind() {
        case reflect.String, reflect.Uint8:
            fmt.Println(v.Elem().Field(i))
        case reflect.Func:
            v.Elem().Field(i).Interface().(func(sth string))("I am saying by reflect")
        }
    }

}

结果:
=== RUN   TestReflect
zhangSan
18
I am saying by reflect
--- PASS: TestReflect (0.00s)
PASS

4.4 Type方法

在 go 的反射系统中,是使用 reflect.Type 这个接口来获取类型信息的。

代码语言:go复制
// Type 是 Go 类型的表示。
//
// 并非所有方法都适用于所有类型。
// 在调用 kind 具体方法之前,先使用 Kind 方法找出类型的种类。因为调用一个方法如果类型不匹配会导致 panic
//
// Type 类型值是可以比较的,比如用 == 操作符。所以它可以用做 map 的 key
// 如果两个 Type 值代表相同的类型,那么它们一定是相等的。
type Type interface {
   // Align 返回该类型在内存中分配时,以字节数为单位的字节数
   Align() int
   
   // FieldAlign 返回该类型在结构中作为字段使用时,以字节数为单位的字节数
   FieldAlign() int
   
   // Method 这个方法返回类型方法集中的第 i 个方法。
   // 如果 i 不在[0, NumMethod()]范围内,就会 panic。
   // 对于非接口类型 T 或 *T,返回的 Method 的 Type 和 Func 字段描述了一个函数,
   // 其第一个参数是接收者,并且只能访问导出的方法。
   // 对于一个接口类型,返回的 Method 的 Type 字段给出的是方法签名,没有接收者,Func字段为nil。
   // 方法是按字典序顺序排列的。
   Method(int) Method

   // MethodByName 返回类型的方法集中具有该名称的方法和一个指示是否找到该方法的布尔值。
   // 对于非接口类型 T 或 *T,返回的 Method 的 Type 和 Func 字段描述了一个函数,
   // 其第一个参数是接收者。
   // 对于一个接口类型,返回的 Method 的 Type 字段给出的是方法签名,没有接收者,Func字段为nil。
   MethodByName(string) (Method, bool)

   // NumMethod 返回使用 Method 可以访问的方法数量。
   // 对于非接口类型,它返回导出方法的数量。
   // 对于接口类型,它返回导出和未导出方法的数量。
   NumMethod() int

   // Name 返回定义类型在其包中的类型名称。
   // 对于其他(未定义的)类型,它返回空字符串。
   Name() string

   // PkgPath 返回一个定义类型的包的路径,也就是导入路径,导入路径是唯一标识包的类型,如 "encoding/base64"。
   // 如果类型是预先声明的(string, error)或者没有定义(*T, struct{}, []int,或 A,其中 A 是一个非定义类型的别名),包的路径将是空字符串。
   PkgPath() string

   // Size 返回存储给定类型的值所需的字节数。它类似于 unsafe.Sizeof.
   Size() uintptr

   // String 返回该类型的字符串表示。
   // 字符串表示法可以使用缩短的包名。
   // (例如,使用 base64 而不是 "encoding/base64")并且它并不能保证类型之间是唯一的。如果是为了测试类型标识,应该直接比较类型 Type。
   String() string

   // Kind 返回该类型的具体种类。
   Kind() Kind

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

   // AssignableTo 表示该类型的值是否可以分配给类型 u。
   AssignableTo(u Type) bool

   // ConvertibleTo 表示该类型的值是否可转换为 u 类型。
   ConvertibleTo(u Type) bool

   // Comparable 表示该类型的值是否具有可比性。
   Comparable() bool
}

5. Reflect性能

首先我们来做一组测试:

代码语言:go复制
func convert(i interface{}) int64 {
   typ := reflect.TypeOf(i)
   switch typ.Kind() {
   case reflect.Int:
      return int64(i.(int))
   case reflect.Int8:
      return int64(i.(int8))
   case reflect.Int16:
      return int64(i.(int16))
   case reflect.Int32:
      return int64(i.(int32))
   case reflect.Int64:
      return i.(int64)
   default:
      panic("not support")
   }
}

func addAny(a, b interface{}) int64 {
   return convert(a)   convert(b)
}

func addInt64(a, b int64) int64 {
   return a   b
}

我们可以通过以下的 benchmark 来对比一下:

代码语言:go复制
BenchmarkAddAny-12    	   141574597	         7.711 ns/op	       0 B/op	       0 allocs/op
BenchmarkAddInt64-12    	1000000000	         0.2935 ns/op	       0 B/op	       0 allocs/op

我们可以看到非常明显的性能差距,addAny() 要比 addInt64() 慢了非常多。

慢的原因:

  1. 编译的时候不知道类型,在运行时通过反射才能做类型判断。
  2. 在运行时查找属性和方法,会有一定的损耗
  3. 先根据 interface{} 创建一个反射对象,然后再做类型判断,再做类型转换,最后再做加法.
  4. 生成的编译指令会多出几十倍
  5. 也有可能在这过程有内存分配的发生(比如FieldByName

6. 总结

  1. reflect 包提供了反射机制,可以在运行时获取变量的类型信息、值信息、方法信息等等
  2. interface{} 实际上包含了两个指针,一个指向类型信息,一个指向值信息。因此,我们可以在运行时通过 interface{} 来获取变量的类型信息、值信息。
  3. reflect.Type 代表一个类型,reflect.Value 代表一个值。通过 reflect.Type 可以获取类型信息,通过 reflect.Value 可以获取值信息
  4. 反射三定律:
    • 反射可以将 interface 类型变量转换成反射对象。
    • 反射可以将反射对象还原成 interface 对象。
    • 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
  5. reflect.Valuereflect.Type 里面都有 Elem 方法,但是它们的作用不一样:
    • reflect.TypeElem 方法返回的是元素类型,只适用于 array、chan、map、pointerslice 类型的 reflect.Type
    • reflect.ValueElem 方法返回的是值,只适用于接口或指针类型的 reflect.Value
  6. 通过 reflect.ValueInterface 方法可以获取到反射对象的原始变量,但是是 interface{} 类型的。
  7. TypeKind 都表示类型,但是 Type 是类型的反射对象,Kind 是 go 类型系统中最基本的一些类型,比如 int、string、struct 等等。

7 参考文献

深入理解 go reflect - 反射基本原理

go interface 设计与实现

深入理解 go reflect - 反射为什么慢

0 人点赞