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

·  阅读 17276

反射概述

反射是这样一种机制,它是可以让我们在程序运行时(runtime)访问、检测和修改对象本身状态或行为的一种能力。 比如,从一个变量推断出其类型信息、以及存储的数据的一些信息,又或者获取一个对象有什么方法可以调用等。 反射经常用在一些需要同时处理不同类型变量的地方,比如序列化、反序列化、ORM 等等,如标准库里面的 json.Marshal

反射基础 - go 的 interface 是怎么存储的?

在正式开始讲解反射之前,我们有必要了解一下 go 里的接口(interface)是怎么存储的。 关于这个问题,在我的另外一篇文章中已经做了很详细的讲解 go interface 设计与实现, 这里不再赘述。但还是简单说一下,go 的接口是由两部分组成的,一部分是类型信息,另一部分是数据信息,如:

var a = 1
var b interface{} = a
复制代码

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

reflect_1.png

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

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
复制代码

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

反射对象 - reflect.Type 和 reflect.Value

知道了 interface{} 的内存结构之后,我们就可以开始讲解反射了。反射的核心是两个对象,分别是 reflect.Typereflect.Value。 它们分别代表了 go 语言中的类型和值。我们可以通过 reflect.TypeOfreflect.ValueOf 来获取到一个变量的类型和值。

var a = 1
t := reflect.TypeOf(a)

var b = "hello"
t1 := reflect.ValueOf(b)
复制代码

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

reflect_2.png

反射定律

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

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

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

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

其实也就是上面的 reflect.Typereflect.Value,我们可以通过 reflect.TypeOfreflect.ValueOf 来获取到一个变量的反射类型和反射值。

var a = 1
typeOfA := reflect.TypeOf(a)
valueOfA := reflect.ValueOf(a)
复制代码

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

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

i := valueOfA.Interface()
fmt.Println(i.(int))
复制代码

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

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

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

那什么情况下一个反射对象是可设置的呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。 在我们传递一个值给 reflect.ValueOf 的时候,如果这个值只是一个普通的变量,那么 reflect.ValueOf 会返回一个不可设置的反射对象。 因为这个值实际上被拷贝了一份,我们如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。 所以 go 语言在这里做了一个限制,如果我们传递进 reflect.ValueOf 的变量是一个普通的变量,那么在我们设置反射对象的值的时候,会报错。 所以在上面这个例子中,我们传递了 x 的指针变量作为参数。这样,运行时就可以找到 x 本身,而不是 x 的拷贝,所以就可以修改 x 的值了。

但同时我们也注意到了,在上面这个例子中,v.CanSet() 返回的是 false,而 v.Elem().CanSet() 返回的是 true。 这是因为,v 是一个指针,而 v.Elem() 是指针指向的值,对于这个指针本身,我们修改它是没有意义的,我们可以设想一下, 如果我们修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样我们的指针变量就不是指向 x 了, 而是指向了其他的变量,这样就不符合我们的预期了。所以 v.CanSet() 返回的是 false

v.Elem().CanSet() 返回的是 true。这是因为 v.Elem() 才是 x 本身,通过 v.Elem() 修改 x 的值是没有问题的。

reflect_3.png

Elem 方法

不知道有多少读者和我一样,在初次使用 go 的反射的时候,被 Elem 这个方法搞得一头雾水。 Elem 方法的作用是什么呢?在回答这个问题之前,我们需要明确一点:reflect.Valuereflect.Type 这两个反射对象都有 Elem 方法,既然是不同的对象,那么它们的作用自然是不一样的。

reflect.Value 的 Elem 方法

reflect.ValueElem 方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 Elem 方法的反射对象,必须是一个指针或者一个接口。 在使用其他类型的 reflect.Value 来调用 Elem 方法的时候,会 panic:

var a = 1
// panic: reflect: call of reflect.Value.Elem on int Value
reflect.ValueOf(a).Elem()

// 不报错
var b = &a
reflect.ValueOf(b).Elem()
复制代码

对于指针很好理解,其实作用类似解引用。而对于接口,还是要回到 interface 的结构本身,因为接口里包含了类型和数据本身,所以 Elem 方法就是获取接口的数据部分(也就是 ifaceeface 中的 data 字段)。

指针类型:

reflect_4.png

接口类型:

reflect_5.png

reflect.Type 的 Elem 方法

reflect.TypeElem 方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于 reflect.Type 来说, 能调用 Elem 方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的 reflect.Type 调用 Elem 方法会 panic

示例:

t1 := reflect.TypeOf([3]int{1, 2, 3}) // 数组 [3]int
fmt.Println(t1.String()) // [3]int
fmt.Println(t1.Elem().String()) // int
复制代码

需要注意的是,如果我们要获取 map 类型 key 的类型信息,需要使用 Key 方法,而不是 Elem 方法。

m := make(map[string]string)
t1 := reflect.TypeOf(m)
fmt.Println(t1.Key().String()) // string
复制代码

Interface 方法

这也是非常常用的一个方法,reflect.ValueInterface 方法的作用是获取反射对象的动态值。 也就是说,如果反射对象是一个指针,那么 Interface 方法会返回指针指向的值。

简单来说,如果 var i interface{} = x,那么 reflect.ValueOf(x).Interface() 就是 i 本身,只不过其类型是 interface{} 类型。

Kind

说到反射,不得不提的另外一个话题就是 go 的类型系统,对于开发者来说,我们可以基于基本类型来定义各种新的类型,如:

// Kind 是 int
type myIny int
// Kind 是 Struct
type Person struct {
    Name string
    Age int
}
复制代码

但是不管我们定义了多少种类型,在 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 的类型系统中都是基本类型的一种,这个基本类型就是 Kind。 也正因为如此,我们可以通过有限的 reflect.TypeKind 来进行类型判断。 也就是说,我们在通过反射来判断变量的类型的时候,只需要枚举 Kind 中的类型,然后通过 reflect.TypeKind 方法来判断即可。

Type 表示的是反射对象(Type 对象是某一个 Kind,通过 Kind() 方法可以获取 Type 的 Kind),Kind 表示的是 go 底层类型系统中的类型。

比如下面的例子:

func display(path string, v reflect.Value) {
   switch v.Kind() {
   case reflect.Invalid:
      fmt.Printf("%s = invalid\n", path)
   case reflect.Slice, reflect.Array:
      for i := 0; i < v.Len(); i++ {
         display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
      }
   case reflect.Struct:
      for i := 0; i < v.NumField(); i++ {
         fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
         display(fieldPath, v.Field(i))
      }
   case reflect.Map:
      for _, key := range v.MapKeys() {
         display(fmt.Sprintf("%s[%s]", path, formatAny(key)), v.MapIndex(key))
      }
   case reflect.Pointer:
      if v.IsNil() {
         fmt.Printf("%s = nil\n", path)
      } else {
         display(fmt.Sprintf("(*%s)", path), v.Elem())
      }
   case reflect.Interface:
      if v.IsNil() {
         fmt.Printf("%s = nil\n", path)
      } else {
         fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
         display(path+".value", v.Elem())
      }
   default:
      fmt.Printf("%s = %s\n", path, formatAny(v))
   }
}
复制代码

我们在开发的时候非常常用的结构体,在 go 的类型系统中,通通都是 Struct 这种类型的。

addressable

go 反射中最后一个很重要的话题是 addressable。在 go 的反射系统中有两个关于寻址的方法:CanAddrCanSet

CanAddr 方法的作用是判断反射对象是否可以寻址,也就是说,如果 CanAddr 返回 true,那么我们就可以通过 Addr 方法来获取反射对象的地址。 如果 CanAddr 返回 false,那么我们就不能通过 Addr 方法来获取反射对象的地址。对于这种情况,我们就无法通过反射对象来修改变量的值。

但是,CanAddrtrue 并不是说 reflect.Value 一定就能修改变量的值了。 reflect.Value 还有一个方法 CanSet,只有 CanSet 返回 true,我们才能通过反射对象来修改变量的值。

那么 CanAddr 背后的含义是什么呢?它意味着我们传递给 reflect.ValueOf 的变量是不是可以寻址的。 也就是说,我们的反射值对象拿到的是不是变量本身,而不是变量的副本。 如果我们是通过 &v 这种方式来创建反射对象的,那么 CanAddr 就会返回 true, 反之,如果我们是通过 v 这种方式来创建反射对象的,那么 CanAddr 就会返回 false

如果想更详细的了解可以参考一下鸟窝的这篇文章 go addressable 详解

获取类型信息 - reflect.Type

概述

reflect.Type 是一个接口,它代表了一个类型。我们可以通过 reflect.TypeOf 来获取一个类型的 reflect.Type 对象。 我们使用 reflect.Type 的目的通常是为了获取类型的信息,比如类型是什么、类型的名称、类型的字段、类型的方法等等。 又或者最常见的场景:结构体中的 jsontag,它是没有语义的,它的作用就是为了在序列化的时候,生成我们想要的字段名。 而这个 tag 就是需要通过反射来获取的。

通用的 Type 方法

在 go 的反射系统中,是使用 reflect.Type 这个接口来获取类型信息的。reflect.Type 这个接口有很多方法,下面这些方法是所有的类型通用的方法:

// 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
}
复制代码

某些类型特定的 Type 方法

下面是某些类型特定的方法,对于这些方法,如果我们使用的类型不对,则会 panic

type Type interface {
   // Bits 以 bits 为单位返回类型的大小。
   // 如果类型的 Kind 不属于:sized 或者 unsized Int, Uint, Float, 或者 Complex,会 panic。
   Bits() int

   // ChanDir 返回一个通道类型的方向。
   // 如果类型的 Kind 不是 Chan,会 panic。
   ChanDir() ChanDir

   // IsVariadic 表示一个函数类型的最终输入参数是否为一个 "..." 可变参数。如果是,t.In(t.NumIn() - 1) 返回参数的隐式实际类型 []T.
   // 更具体的,如果 t 代表 func(x int, y ... float64),那么:
   // t.NumIn() == 2
   // t.In(0)是 "int" 的 reflect.Type 反射类型。
   // t.In(1)是 "[]float64" 的 reflect.Type 反射类型。
   // t.IsVariadic() == true
   // 如果类型的 Kind 不是 Func,IsVariadic 会 panic
   IsVariadic() bool

   // Elem 返回一个 type 的元素类型。
   // 如果类型的 Kind 不是 Array、Chan、Map、Ptr 或 Slice,就会 panic
   Elem() Type

   // Field 返回一个结构类型的第 i 个字段。
   // 如果类型的 Kind 不是 Struct,就会 panic。
   // 如果 i 不在 [0, NumField()) 范围内也会 panic。
   Field(i int) StructField

   // FieldByIndex 返回索引序列对应的嵌套字段。它相当于对每一个 index 调用 Field。
   // 如果类型的 Kind 不是 Struct,就会 panic。
   FieldByIndex(index []int) StructField

   // FieldByName 返回给定名称的结构字段和一个表示是否找到该字段的布尔值。
   FieldByName(name string) (StructField, bool)

   // FieldByNameFunc 返回一个能满足 match 函数的带有名称的 field 字段。布尔值表示是否找到。
   FieldByNameFunc(match func(string) bool) (StructField, bool)

   // In 返回函数类型的第 i 个输入参数的类型。
   // 如果类型的 Kind 不是 Func 类型会 panic。
   // 如果 i 不在 [0, NumIn()) 的范围内,会 panic。
   In(i int) Type

   // Key 返回一个 map 类型的 key 类型。
   // 如果类型的 Kind 不是 Map,会 panic。
   Key() Type

   // Len 返回一个数组类型的长度。
   // 如果类型的 Kind 不是 Array,会 panic。
   Len() int

   // NumField 返回一个结构类型的字段数目。
   // 如果类型的 Kind 不是 Struct,会 panic。
   NumField() int

   // NumIn 返回一个函数类型的输入参数数。
   // 如果类型的 Kind 不是Func.NumIn(),会 panic。
   NumIn() int

   // NumOut 返回一个函数类型的输出参数数。
   // 如果类型的 Kind 不是 Func.NumOut(),会 panic。
   NumOut() int

   // Out 返回一个函数类型的第 i 个输出参数的类型。
   // 如果类型的 Kind 不是 Func,会 panic。
   // 如果 i 不在 [0, NumOut()) 的范围内,会 panic。
   Out(i int) Type
}
复制代码

创建 reflect.Type 的方式

我们可以通过下面的方式来获取变量的类型信息(创建 reflect.Type 的方式):

reflect_6.png

获取值信息 - reflect.Value

概述

reflect.Value 是一个结构体,它代表了一个值。 我们使用 reflect.Value 可以实现一些接收多种类型参数的函数,又或者可以让我们在运行时针对值的一些信息来进行修改。 常常用在接收 interface{} 类型参数的方法中,因为参数是接口类型,所以我们可以通过 reflect.ValueOf 来获取到参数的值信息。 比如类型、大小、结构体字段、方法等等。

同时,我们可以对这些获取到的反射值进行修改。这也是反射的一个重要用途。

reflect.Value 的方法

reflect.Value 这个 Sreuct 同样有很多方法:具体可以分为以下几类:

  1. 设置值的方法:Set*SetSetBoolSetBytesSetCapSetComplexSetFloatSetIntSetLenSetMapIndexSetPointerSetStringSetUint。通过这类方法,我们可以修改反射值的内容,前提是这个反射值得是合适的类型。CanSet 返回 true 才能调用这类方法
  2. 获取值的方法:InterfaceInterfaceDataBoolBytesComplexFloatIntStringUint。通过这类方法,我们可以获取反射值的内容。前提是这个反射值是合适的类型,比如我们不能通过 complex 反射值来调用 Int 方法(我们可以通过 Kind 来判断类型)。
  3. map 类型的方法:MapIndexMapKeysMapRangeMapSet
  4. chan 类型的方法:CloseRecvSendTryRecvTrySend
  5. slice 类型的方法:LenCapIndexSliceSlice3
  6. struct 类型的方法:NumFieldNumMethodFieldFieldByIndexFieldByNameFieldByNameFunc
  7. 判断是否可以设置为某一类型:CanConvertCanComplexCanFloatCanIntCanInterfaceCanUint
  8. 方法类型的方法:MethodMethodByNameCallCallSlice
  9. 判断值是否有效:IsValid
  10. 判断值是否是 nilIsNil
  11. 判断值是否是零值:IsZero
  12. 判断值能否容纳下某一类型的值:OverflowOverflowComplexOverflowFloatOverflowIntOverflowUint
  13. 反射值指针相关的方法:AddrCanAddrtrue 才能调用)、UnsafeAddrPointerUnsafePointer
  14. 获取类型信息:TypeKind
  15. 获取指向元素的值:Elem
  16. 类型转换:Convert

Len 也适用于 slicearraychanmapstring 类型的反射值。

创建 reflect.Value 的方式

我们可以通过下面的方式来获取变量的值信息(创建 reflect.Value 的方式):

reflect_7.png

总结

  • reflect 包提供了反射机制,可以在运行时获取变量的类型信息、值信息、方法信息等等。
  • go 中的 interface{} 实际上包含了两个指针,一个指向类型信息,一个指向值信息。正因如此,我们可以在运行时通过 interface{} 来获取变量的类型信息、值信息。
  • reflect.Type 代表一个类型,reflect.Value 代表一个值。通过 reflect.Type 可以获取类型信息,通过 reflect.Value 可以获取值信息。
  • 反射三定律:
    • 反射可以将 interface 类型变量转换成反射对象。
    • 反射可以将反射对象还原成 interface 对象。
    • 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
  • reflect.Valuereflect.Type 里面都有 Elem 方法,但是它们的作用不一样:
    • reflect.TypeElem 方法返回的是元素类型,只适用于 array、chan、map、pointer 和 slice 类型的 reflect.Type
    • reflect.ValueElem 方法返回的是值,只适用于接口或指针类型的 reflect.Value
  • 通过 reflect.ValueInterface 方法可以获取到反射对象的原始变量,但是是 interface{} 类型的。
  • TypeKind 都表示类型,但是 Type 是类型的反射对象,Kind 是 go 类型系统中最基本的一些类型,比如 intstringstruct 等等。
  • 如果我们想通过 reflect.Value 来修改变量的值,那么 reflect.Value 必须是可设置的(CanSet)。同时如果想要 CanSet 为 true,那么我们的变量必须是可寻址的。
  • 我们有很多方法可以创建 reflect.Typereflect.Value,我们需要根据具体的场景来选择合适的方法。
  • reflect.Typereflect.Value 里面,都有一部分方法是通用的,也有一部分只适用于特定的类型。如果我们想要调用那些适用于特定类型的方法,那么我们必须先判断 reflect.Typereflect.Value 的类型(这里说的是 Kind),然后再调用。
复制代码
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改