初探Go反射三大定律
目录 ▼
😀 最近在研究Go语言的源码,看到反射部分,结合The Go Blog系列的《The Laws of Reflection》,以及Go 1.15 中 src/reflect 部分源码,记录下对于Go 反射的一些见解。
〇、前情提要
Go的反射基础是接口和类型系统。学习之前,最好先了解Go接口的实现,另外,反射的API也很多,了解其核心部分即可,一些其他API可以在通过源码分析来了解。笔者在本文中只是结合源码分析初探Go反射的三大定律。

在反射的世界里,我们拥有了获取一个对象的类型,属性及方法的能力。
一、反射的基本概念
什么是反射(reflect)
维基百科如是说明:
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
今天我们重点讲讲Go语言的三大反射定律
为什么要使用反射?
-
Go 不支持泛型,通过反射可以间接实现泛型的需求
-
借助反射可以极大简化设计,不需要对每一种场景做硬编码处理
-
反射提供了一种程序了解自己和改变自己的能力,这为一些测试工具的开发提供了有力的支持。
二、反射的两种基本数据结构
Go的反射巧妙地借助了实例到接口的转换所使用的数据结构,首先将实例传给内部的空接口,实际上是将实例类型转换为接口可以表述的数据结构emptyInterface,反射基于这个转换后的数据结构来访问和操作实例的值和类型。实例传递给interface{} 类型,编译器会进行一个内部的转换,自动创建相关类型数据结构。
2.1 reflect.Type
// Type 是 Go 类型的表示
//
// 并非所有方法都适用于所有类型。 每种方法的文档中都注明了限制(如果有)。
// 在调用特定于种类的方法之前,使用 Kind 方法找出类型的种类。
// 调用不适合该类型的方法会导致运行时panic。
//
// 类型值是可比较的,例如使用 == 运算符,因此它们可以用作映射键。
// 如果两个 Type 值表示相同的类型,则它们相等。
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Name() string
PkgPath() string
Size() uintptr
String() string
Kind() Kind
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
Bits() int
ChanDir() ChanDir
IsVariadic() bool
Elem() Type
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
In(i int) Type
Key() Type
Len() int
NumField() int
NumIn() int
NumOut() int
Out(i int) Type
}
源码见src/reflect/type.go
可以看到,Type是一个接口,它里面定义了28个方法
为什么反射接口返回的是一个Type接口类型,而不是直接返回具体的类型结构呢
-
一是因为类型信息是一个只读的信息,不可能动态地修改类型的相关信息,那太不安全了;
-
二是因为不同的类型,类型定义也不一样,使用接口这一抽象数据结构能够进行统一的抽象。
2.2 reflect.Value
// Value 是 Go 值的反射接口。
//
// 并非所有方法都适用于所有类型的值。 每种方法的文档中都注明了限制(如果有)。 在调用种类特定的方法之前,使用 Kind 方法找出值的种类。 调用不适合该类型的方法会导致运行时panic。(这里与Type是一致的)
//
// 零值表示no value。
// 它的 IsValid 方法返回 false,它的 Kind 方法返回 Invalid,它的 String 方法返回“<invalid Value>”,所有其他方法都会 panic。
// 大多数函数和方法从不返回无效值。如果有,其文档明确说明了条件。
//
// 一个值可以被多个 goroutine 同时使用,前提是底层的 Go 值可以同时用于等效的直接操作。
// 要比较两个值,请比较接口方法的结果。
// 在两个值上使用 == 不会比较它们代表的基础值。
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
源码见src/reflect/value.go
可以发现,Value是一个结构体,它包含三个字段
-
typ 值的类型指针
-
ptr 指向值的指针
-
flag 标记字段
另外,Value 还有68个方法,这里先就不赘述了,包括61个 public 方法和7个 private 方法
Addr
Bool
Bytes
CanAddr
CanSet
Call
CallSlice
Cap
Close
Complex
Elem
Field
FieldByIndex
FieldByName
FieldByNameFunc
Float
Index
Int
CanInterface
Interface
InterfaceData
IsNil
IsValid
IsZero
Kind
Len
MapIndex
MapKeys
MapRange
Method
NumMethod
MethodByName
NumField
OverflowComplex
OverflowFloat
OverflowInt
OverflowUint
Pointer
Recv
Send
Set
SetBool
SetBytes
SetComplex
SetFloat
SetInt
SetLen
SetCap
SetMapIndex
SetUint
SetPointer
SetString
Slice
Slice3
String
TryRecv
TrySend
Type
Uint
UnsafeAddr
Convert
pointer
runes
call
recv
send
setRunes
assignTo
三、反射三定律
《The Laws of Reflection》原文提到反射的三个定律
翻译过来就是
-
反射可以从接口值得到反射对象
-
反射可以从反射对象对到接口值
-
若要修改一个反射对象,则其值必须可修改
是不是感觉听起来很绕,我们一一解读。
3.1 第一定律
📌 反射可以从接口值得到反射对象
从接口对象获取对应的反射对象可以使用reflect.TypeOf()与reflect.ValueOf()分别获取反射的类型对象与反射的值对象。也就是第二节中的 reflect.Type与reflect.Value

我们看一个例子:
package main
import (
"fmt"
"reflect"
)
type S struct {
a int
b float64
}
func main() {
var x float64 = 3.4
var y int = 100
var s = S{
a: 1,
b: 90.9,
}
fmt.Println("type:", reflect.TypeOf(x))
fmt.Println("value:", reflect.ValueOf(x).String())
fmt.Println("type:", reflect.TypeOf(y))
fmt.Println("value:", reflect.ValueOf(y).String())
fmt.Println("type:", reflect.TypeOf(s))
fmt.Println("value:", reflect.ValueOf(s).String())
}
// 结果
type: float64
value: <float64 Value>
type: int
value: <int Value>
type: main.S
value: <main.S Value>
- TypeOf()
// 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))
return toType(eface.typ)
}
- ValueOf
// 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{}
}
// 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)
}
可以看到两个函数都是传递的一个空接口类型的值,参数为空接口时,可以接受任何类型。所以这里传入的类型可以任何类型的值。关于空接口的知识点不在本文中阐述。
3.2 第二定律
📌 反射可以从反射对象对到接口值
刚好和第一定律相反。

// Interface returns v's current value as an interface{}.
// It is equivalent to:
// var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}
Value 有方法 Interface() 支持从 reflect.Value 类型 转为 接口变量。
另外,还提供了丰富的方法来实现从 Value到 接口对象实例的转换。
func (v Value) Bool() bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64
注意:Type是不支持逆向的,因为里面只包含类型信息,所以无法逆向转化。
我们看一个例子:
package main
import (
"fmt"
"reflect"
)
type SS struct {
a int
b float64
}
func main() {
var x float64 = 3.4
var y int = 100
var s = SS{
a: 1,
b: 90.9,
}
v1 := reflect.ValueOf(x)
v2 := reflect.ValueOf(y)
v3 := reflect.ValueOf(s)
i1 := v1.Interface()
i2 := v2.Interface()
i3 := v3.Interface()
fmt.Printf("type: %T, value: %v\n", i1, i1)
fmt.Printf("type: %T, value: %v\n", i2, i2)
fmt.Printf("type: %T, value: %v\n", i3, i3)
}
// 结果
type: float64, value: 3.4
type: int, value: 100
type: main.SS, value: {1 90.9}
如果想要获取最初的类型,可以用类型断言进行转换。
i3 := v3.Interface().(SS)
3.3 第三定律
📌 若要修改一个反射对象,则其值必须可修改
这里提到一个可修改的概念,也就是settable
首先,我们应该了解,在Go中所有的传递都是值传递。值变量传递拷贝的值,指针变量传递时的指针地址的拷贝。
Value 值在什么情况下是可以修改?我们知道接口对象传递给接口的是一个完全的值拷贝,如果调用反射方法reflect.ValueOf() 传进去的是一个值类型变量,则获得的Value实际上是原对象的一个副本,这个Value是无法被修改的。如果传进去的是一个指针,那么Value是可以修改的。
Value 值的修改涉及如下两个方法:
// CanSet reports whether the value of v can be changed.
// A Value can be changed only if it is addressable and was not
// obtained by the use of unexported struct fields.
// If CanSet returns false, calling Set or any type-specific
// setter (e.g., SetBool, SetInt) will panic.
func (v Value) CanSet() bool {
return v.flag&(flagAddr|flagRO) == flagAddr
}
// Set assigns x to the value v.
// It panics if CanSet returns false.
// As in Go, x's value must be assignable to v's type.
func (v Value) Set(x Value) {
v.mustBeAssignable()
x.mustBeExported() // do not let unexported x leak
var target unsafe.Pointer
if v.kind() == Interface {
target = v.ptr
}
x = x.assignTo("reflect.Set", v.typ, target)
if x.flag&flagIndir != 0 {
typedmemmove(v.typ, v.ptr, x.ptr)
} else {
*(*unsafe.Pointer)(v.ptr) = x.ptr
}
}
CanSet() 可以确定一个 Value 是否可以修改
Set() 方法用于修改 Value,另外还有其他不同类型的Set方法

我们看一下例子:
package main
import (
"fmt"
"reflect"
)
type User1 struct {
ID int
Name string
Age int
}
func main() {
u := User1{
ID: 1,
Name: "eachen",
Age: 26,
}
va := reflect.ValueOf(u)
vb := reflect.ValueOf(&u)
// 值类型是不可修改的
fmt.Println(va.CanSet(), va.FieldByName("Name").CanSet())
// 指针类型是可修改的
fmt.Println(vb.CanSet(), vb.Elem().FieldByName("Name").CanSet())
fmt.Printf("%v\n\n", vb)
name := "kuang"
vc:= reflect.ValueOf(name)
vb.Elem().FieldByName("Name").Set(vc)
fmt.Printf("%v\n\n", vb)
}
// 结果
false false
false true
&{1 eachen 26}
&{1 kuang 26}
四、reflect 转化常用API
到此,我们已经了解了反射的三大定律。我们来总结下
下图是接口对象、Type、Value之间的转化关系以及使用到的API:

图中提到的一些 API:
-
从实例到Value
-
从实例到Type
-
从Type 到 Value
-
从 Value 到 Type
-
从Value 到实例
-
从Value 的指针到值
-
Type 指针和值的相互转换
五、深入挖掘rtype
首先我们看下一个最通用的类型公共信息 rtype,它是每一种基础类型的一个成员类型。
如果有同学看过 runtime 的源码,那么可能知道,rtype 和 _type 是同一个结构体
// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
它实现了 reflect.Type接口

可以看到,其他的基本类型都有一个rtype 类型的成员变量
关于Type类型中的主要方法
- 所有类型通用的方法:
// Name returns the type's name within its package for a defined type.
// For other (non-defined) types it returns the empty string.
Name() string
// Kind returns the specific kind of this type.
Kind() Kind
// Implements reports whether the type implements the interface type u.
Implements(u Type) bool
// AssignableTo reports whether a value of the type is assignable to type u.
AssignableTo(u Type) bool
// ConvertibleTo reports whether a value of the type is convertible to type u.
ConvertibleTo(u Type) bool
// Comparable reports whether values of this type are comparable.
Comparable() bool
// Method returns the i'th method in the type's method set.
// It panics if i is not in the range [0, NumMethod()).
//
// For a non-interface type T or *T, the returned Method's Type and Func
// fields describe a function whose first argument is the receiver.
//
// For an interface type, the returned Method's Type field gives the
// method signature, without a receiver, and the Func field is nil.
//
// Only exported methods are accessible and they are sorted in
// lexicographic order.
Method(int) Method
// MethodByName returns the method with that name in the type's
// method set and a boolean indicating if the method was found.
//
// For a non-interface type T or *T, the returned Method's Type and Func
// fields describe a function whose first argument is the receiver.
//
// For an interface type, the returned Method's Type field gives the
// method signature, without a receiver, and the Func field is nil.
MethodByName(string) (Method, bool)
// NumMethod returns the number of exported methods in the type's method set.
NumMethod() int
// PkgPath returns a defined type's package path, that is, the import path
// that uniquely identifies the package, such as "encoding/base64".
// If the type was predeclared (string, error) or not defined (*T, struct{},
// []int, or A where A is an alias for a non-defined type), the package path
// will be the empty string.
PkgPath() string
// Size returns the number of bytes needed to store
// a value of the given type; it is analogous to unsafe.Sizeof.
Size() uintptr
// String returns a string representation of the type.
// The string representation may use shortened package names
// (e.g., base64 instead of "encoding/base64") and is not
// guaranteed to be unique among types. To test for type identity,
// compare the Types directly.
String() string
- 不同基础类型的专有方法
如果调用错误,那么会panic