深入理解 go reflect – 反射基本原理
反射概述
反射是这样一种机制,它是能够让咱们在程序运行时(runtime)拜访、检测和修正目标自身状况或行为的一种能力。
比方,从一个变量推断出其类型信息、以及存储的数据的一些信息,又或许获取一个目标有什么办法能够调用等。
反射常常用在一些需求一起处理不同类型变量的当地,比方序列化、反序列化、ORM
等等,如规范库里边的 json.Marshal
。
反射基础 – go 的 interface 是怎样存储的?
在正式开端解说反射之前,咱们有必要了解一下 go 里的接口(interface
)是怎样存储的。
关于这个问题,在我的别的一篇文章中已经做了很详细的解说 go interface 设计与完成,
这儿不再赘述。但还是简略说一下,go 的接口是由两部分组成的,一部分是类型信息,另一部分是数据信息,如:
var a = 1
var b interface{} = a
关于这个比方,b
的类型信息是 int
,数据信息是 1
,这两部分信息都是存储在 b
里边的。b
的内存结构如下:
在上图中,b
的类型实践上是 eface
,它是一个空接口,它的界说如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
也便是说,一个 interface{} 中实践上既包含了变量的类型信息,也包含了类型的数据。 正由于如此,咱们才能够经过反射来获取到变量的类型信息,以及变量的数据信息。
反射目标 – reflect.Type 和 reflect.Value
知道了 interface{}
的内存结构之后,咱们就能够开端解说反射了。反射的核心是两个目标,别离是 reflect.Type
和 reflect.Value
。
它们别离代表了 go 言语中的类型和值。咱们能够经过 reflect.TypeOf
和 reflect.ValueOf
来获取到一个变量的类型和值。
var a = 1
t := reflect.TypeOf(a)
var b = "hello"
t1 := reflect.ValueOf(b)
咱们去看一下 TypeOf
和 ValueOf
的源码会发现,这两个办法都接纳一个 interface{}
类型的参数,然后回来一个 reflect.Type
和 reflect.Value
类型的值。这也便是为什么咱们能够经过 reflect.TypeOf
和 reflect.ValueOf
来获取到一个变量的类型和值的原因。
反射定律
在 go 官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:
- 反射能够将
interface
类型变量转换成反射目标。 - 反射能够将反射目标还原成
interface
目标。 - 假如要修正反射目标,那么反射目标有必要是可设置的(
CanSet
)。
关于这三条定律,官方博客已经有了比较完整的阐述,感兴趣的能够去看一下官方博客的文章。这儿简略阐述一下:
反射能够将 interface
类型变量转换成反射目标。
其实也便是上面的 reflect.Type
和 reflect.Value
,咱们能够经过 reflect.TypeOf
和 reflect.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
的值是没有问题的。
Elem 办法
不知道有多少读者和我一样,在初次运用 go 的反射的时分,被 Elem
这个办法搞得一头雾水。
Elem
办法的效果是什么呢?在回答这个问题之前,咱们需求明确一点:reflect.Value
和 reflect.Type
这两个反射目标都有 Elem
办法,既然是不同的目标,那么它们的效果自然是不一样的。
reflect.Value 的 Elem 办法
reflect.Value
的 Elem
办法的效果是获取指针指向的值,或许获取接口的动态值。也便是说,能调用 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
办法便是获取接口的数据部分(也便是 iface
或 eface
中的 data
字段)。
指针类型:
接口类型:
reflect.Type 的 Elem 办法
reflect.Type
的 Elem
办法的效果是获取数组、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.Value
的 Interface
办法的效果是获取反射目标的动态值。
也便是说,假如反射目标是一个指针,那么 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.Type
的 Kind
来进行类型判别。
也便是说,咱们在经过反射来判别变量的类型的时分,只需求枚举 Kind
中的类型,然后经过 reflect.Type
的 Kind
办法来判别即可。
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 的反射体系中有两个关于寻址的办法:CanAddr
和 CanSet
。
CanAddr
办法的效果是判别反射目标是否能够寻址,也便是说,假如 CanAddr
回来 true
,那么咱们就能够经过 Addr
办法来获取反射目标的地址。
假如 CanAddr
回来 false
,那么咱们就不能经过 Addr
办法来获取反射目标的地址。关于这种状况,咱们就无法经过反射目标来修正变量的值。
可是,CanAddr
是 true
并不是说 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
的目的一般是为了获取类型的信息,比方类型是什么、类型的称号、类型的字段、类型的办法等等。
又或许最常见的场景:结构体中的 json
的 tag
,它是没有语义的,它的效果便是为了在序列化的时分,生成咱们想要的字段名。
而这个 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.Value
概述
reflect.Value
是一个结构体,它代表了一个值。
咱们运用 reflect.Value
能够完成一些接纳多品种型参数的函数,又或许能够让咱们在运行时针对值的一些信息来进行修正。
常常用在接纳 interface{}
类型参数的办法中,由于参数是接口类型,所以咱们能够经过 reflect.ValueOf
来获取到参数的值信息。
比方类型、大小、结构体字段、办法等等。
一起,咱们能够对这些获取到的反射值进行修正。这也是反射的一个重要用途。
reflect.Value 的办法
reflect.Value
这个 Sreuct
相同有许多办法:详细能够分为以下几类:
- 设置值的办法:
Set*
:Set
、SetBool
、SetBytes
、SetCap
、SetComplex
、SetFloat
、SetInt
、SetLen
、SetMapIndex
、SetPointer
、SetString
、SetUint
。经过这类办法,咱们能够修正反射值的内容,条件是这个反射值得是适宜的类型。CanSet 回来 true 才能调用这类办法 - 获取值的办法:
Interface
、InterfaceData
、Bool
、Bytes
、Complex
、Float
、Int
、String
、Uint
。经过这类办法,咱们能够获取反射值的内容。条件是这个反射值是适宜的类型,比方咱们不能经过complex
反射值来调用Int
办法(咱们能够经过Kind
来判别类型)。 - map 类型的办法:
MapIndex
、MapKeys
、MapRange
、MapSet
。 - chan 类型的办法:
Close
、Recv
、Send
、TryRecv
、TrySend
。 - slice 类型的办法:
Len
、Cap
、Index
、Slice
、Slice3
。 - struct 类型的办法:
NumField
、NumMethod
、Field
、FieldByIndex
、FieldByName
、FieldByNameFunc
。 - 判别是否能够设置为某一类型:
CanConvert
、CanComplex
、CanFloat
、CanInt
、CanInterface
、CanUint
。 - 办法类型的办法:
Method
、MethodByName
、Call
、CallSlice
。 - 判别值是否有用:
IsValid
。 - 判别值是否是
nil
:IsNil
。 - 判别值是否是零值:
IsZero
。 - 判别值能否容纳下某一类型的值:
Overflow
、OverflowComplex
、OverflowFloat
、OverflowInt
、OverflowUint
。 - 反射值指针相关的办法:
Addr
(CanAddr
为true
才能调用)、UnsafeAddr
、Pointer
、UnsafePointer
。 - 获取类型信息:
Type
、Kind
。 - 获取指向元素的值:
Elem
。 - 类型转换:
Convert
。
Len
也适用于slice
、array
、chan
、map
、string
类型的反射值。
创立 reflect.Value 的办法
咱们能够经过下面的办法来获取变量的值信息(创立 reflect.Value
的办法):
总结
-
reflect
包供给了反射机制,能够在运行时获取变量的类型信息、值信息、办法信息等等。 - go 中的
interface{}
实践上包含了两个指针,一个指向类型信息,一个指向值信息。正因如此,咱们能够在运行时经过interface{}
来获取变量的类型信息、值信息。 -
reflect.Type
代表一个类型,reflect.Value
代表一个值。经过reflect.Type
能够获取类型信息,经过reflect.Value
能够获取值信息。 - 反射三定律:
- 反射能够将
interface
类型变量转换成反射目标。 - 反射能够将反射目标还原成
interface
目标。 - 假如要修正反射目标,那么反射目标有必要是可设置的(
CanSet
)。
- 反射能够将
-
reflect.Value
和reflect.Type
里边都有Elem
办法,可是它们的效果不一样:-
reflect.Type
的Elem
办法回来的是元素类型,只适用于 array、chan、map、pointer 和 slice 类型的reflect.Type
。 -
reflect.Value
的Elem
办法回来的是值,只适用于接口或指针类型的reflect.Value
。
-
- 经过
reflect.Value
的Interface
办法能够获取到反射目标的原始变量,可是是interface{}
类型的。 -
Type
和Kind
都表明类型,可是Type
是类型的反射目标,Kind
是 go 类型体系中最根本的一些类型,比方int
、string
、struct
等等。 - 假如咱们想经过
reflect.Value
来修正变量的值,那么reflect.Value
有必要是可设置的(CanSet
)。一起假如想要CanSet
为 true,那么咱们的变量有必要是可寻址的。 - 咱们有许多办法能够创立
reflect.Type
和reflect.Value
,咱们需求根据详细的场景来挑选适宜的办法。 -
reflect.Type
和reflect.Value
里边,都有一部分办法是通用的,也有一部分只适用于特定的类型。假如咱们想要调用那些适用于特定类型的办法,那么咱们有必要先判别reflect.Type
或reflect.Value
的类型(这儿说的是Kind
),然后再调用。