1 反射是什么
我们在编写代码时,很清晰的知道程序中定义的变量(golang中,变量是存储值的地方)的变量名是什么,数据类型是什么,如果数据类型是一个结构体(struct),也清晰的知道结构体有哪些成员字段(Field),即使是一个接口(interface)变量,我们也可以通过分析程序的执行流程确定接口的动态类型(或者说具体类型,Concrete Type)。但是经过编译器、连接器、加载器处理后运行程序时,程序执行者(CPU)看到的是地址(确定到内存的什么位置获取指令和数据)和值(内存指定地址处存储的二进制比特序列),然后执行指令规定的动作。也就是说,我们在编程时所定义的变量名啊,数据类型什么的是面向编译器和编程人员的,是帮助大家理解的标记符,他们不是面向CPU的。在程序运行时(运行态),CPU并不知道执行的指令和数据相应的变量名和数据类型分别是什么。golang语言的反射(reflect)功能就是一种让程序在运行时具备获取值的数据类型、名字、布局(比如结构体有哪些成员,这些成员的值和类型等信息)、方法等信息并修改值的内容的能力
。编译是将变量名、数据类型映射到地址和值,反射是将地址和值映射到编程时定义的变量名和数据类型,与编译的过程相反。
反射功能极大地增强了编程语言的表现能力,尤其在面向输入数据非常灵活,无法事先确定具体的类型和值时,通过反射可以非常灵活的统一处理不同的数据类型和值,使代码更加紧凑和具备更广泛的适应性,不需要根据每一种情况编写相应的代码。比较典型和常用的例子是json字符串的反序列化,输入的json字符串是千变万化的,输出的对象类型也是无法穷举的,使用反射功能就可以轻松的将json字符串与用户指定的类型对象关联起来。
在编程语言中,与反射相近的一个概念是内省(inspect)。一般来说,內省只能查看值的数据类型、布局等信息,但不能修改值的内容。但是反射既能查看也能修改。
2 反射的基本操作
golang在reflect
包中定义了两个接口:Type
和Value
,通过它们来统一表示类型和值。
2.1 Type
golang在reflect包中定义了很多实现了Type接口的结构体,他们分别存储了不同的数据类型的Type信息,但是这些Type接口实现都是无法包外访问的,所以我们无法直接创建这些类型的变量。同时,reflect包提供了一个方法TypeOf()来获取任意对象的Type。TypeOf()方法的定义为:
func TypeOf(i any) Type
这样,我们可以通过以下方法获取变量的Type:
vInt := 100
typeOfInt := reflect.TypeOf(vInt)
获取了变量的Type后,我们就可以通过Type接口的Name()和String()方法获取类型的名字和格式化字符串信息,如下:
fmt.Printf("typeOfInt name: %s, formatted string: %s\n", typeOfInt.Name(), typeOfInt.String())
结果输出正是数据类型的名字:
typeOfInt name: int, formatted string: int
我们再看看一些常见数据类型的Name()和String()方法输出。
type Enum int
type Person struct {
Name string
}
func main() {
var vUint uint = 123
typeOfUint := reflect.TypeOf(vUint)
fmt.Printf("typeOfUint name: %s, formatted string: %s\n", typeOfUint.Name(), typeOfUint.String())
vStr := "123"
typeOfString := reflect.TypeOf(vStr)
fmt.Printf("typeOfString name: %s, formatted string: %s\n", typeOfString.Name(), typeOfString.String())
var vEnum Enum = 100
typeOfEnum := reflect.TypeOf(vEnum)
fmt.Printf("typeOfEnum name: %s, formatted string: %s\n", typeOfEnum.Name(), typeOfEnum.String())
vStruct := Person{Name: "Jon"}
typeOfStruct := reflect.TypeOf(vStruct)
fmt.Printf("typeOfStruct name: %s, formatted string: %s\n", typeOfStruct.Name(), typeOfStruct.String())
vArray := [3]int{}
typeOfArray := reflect.TypeOf(vArray)
fmt.Printf("typeOfArray name: %s, formatted string: %s\n", typeOfArray.Name(), typeOfArray.String())
vSlice := []int{}
typeOfSlice := reflect.TypeOf(vSlice)
fmt.Printf("typeOfSlice name: %s, formatted string: %s\n", typeOfSlice.Name(), typeOfSlice.String())
vMap := map[string]int{}
typeOfMap := reflect.TypeOf(vMap)
fmt.Printf("typeOfMap name: %s, formatted string: %s\n", typeOfMap.Name(), typeOfMap.String())
vPtr := &vUint
typeOfPtr := reflect.TypeOf(vPtr)
fmt.Printf("typeOfPtr name: %s, formatted string: %s\n", typeOfPtr.Name(), typeOfPtr.String())
}
输出:
typeOfUint name: uint, formatted string: uint
typeOfString name: string, formatted string: string
typeOfEnum name: Enum, formatted string: main.Enum
typeOfStruct name: Person, formatted string: main.Person
typeOfArray name: , formatted string: [3]int
typeOfSlice name: , formatted string: []int
typeOfMap name: , formatted string: map[string]int
typeOfPtr name: , formatted string: *uint
从输出结果可以看出,引用类型的Name值为空,自定义数据类型的格式化字符串包含了包名字
再来看看接口的例子
type Work interface {
DoWork()
}
type Engineer struct {
Name string
}
func (e Engineer) DoWork() {
fmt.Printf("Hello, %v", e.Name)
}
type Artist struct {
Name string
}
func (e *Artist) DoWork() {
fmt.Printf("Hello, %v", e.Name)
}
func main() {
var vInterface Work
vInterface = Engineer{}
typeOfInterface := reflect.TypeOf(vInterface)
fmt.Printf("typeOfInterface name: %s, formatted string: %s\n", typeOfInterface.Name(), typeOfInterface.String())
vInterface = &Artist{}
typeOfPtrInterface := reflect.TypeOf(vInterface)
fmt.Printf("typeOfPtrInterface name: %s, formatted string: %s\n", typeOfPtrInterface.Name(), typeOfPtrInterface.String())
}
输出:
typeOfInterface name: Engineer, formatted string: main.Engineer
typeOfPtrInterface name: , formatted string: *main.Artist
可见接口的Type是接口的动态类型,而不是接口类型本身(静态类型)。事实上,接口的Type永远是接口的具体实现类型
因为Type接口实现了fmt.Stringer接口,所以格式化字符串时可以用%T
占位符得到变量的类型,如下所示:
vInt := 2
fmt.Printf("formatted string: %T, %T\n", vInt, &vInt)
输出:
formatted string: int, *int
2.2 Value
与Type接口类似,reflect包提供了ValueOf(i any)
函数获取任意变量的Value,格式化字符串时使用%v
占位符打印变量值的内容,同时Value也实现了fmt.Stringer接口。举个简单的例子:
vInt := 2
valueOfInt := reflect.ValueOf(vInt)
fmt.Printf("formatted string: %s, %v, stringer: %s\n", valueOfInt, valueOfInt, valueOfInt.String())
输出:
formatted string: %!s(int=2), 2, stringer: <int Value>
2.3 Type、Value、Interface相互转换
如上所述,我们可以通过reflect.TypeOf()、reflect.ValueOf()方法获取interface的Type和Value。同时我们可以通过Value的Type()方法得到Value的Type,通过Value的Interface()方法得到interface值
vInt := 2
valueOfInt := reflect.ValueOf(vInt)
typeOfInt := valueOfInt.Type()
fmt.Printf("typeOfInt: %s\n", typeOfInt.String())
vInterface := valueOfInt.Interface()
x := vInterface.(int) + 1
fmt.Printf("x value: %d\n", x)
结果输出:
typeOfInt: int
x value: 3
其实,Value和interface都包含了对象的类型和值信息,但是Value接口提供了更加丰富的方法来查看和操作对象的类型和值,后面会详细介绍这些接口。但是Type只包含了类型信息,所以没有从Type获取Value和interface值的方法
2.4 Type和Value的Kind
因为golang支持自定义数据类型,所以应用程序的数据类型可以是无数的,也就是说Type代表的数据类型是无数的。好消息是,我们可以把系统所有的数据类型归类为少量的固定的几个类型(kind),并可以通过Type或Value的Kind()方法获取相应数据类型所属的kind。系统所有的数据类型可以归属为以下的kind:
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
)
知道Type或Value所属的kind非常有用,我们通过根据Type或Value的kind值做相应的处理,如下所示:
vInt := 2
valueOfInt := reflect.ValueOf(vInt)
kindOfInt := valueOfInt.Kind()
switch kindOfInt {
case reflect.Int:
fmt.Printf("%d", valueOfInt.Interface().(int))
case reflect.String:
// do something
// case other kind
}
在Kind的枚举列表中,我们还看到了叫做Interface的枚举值。但是reflect.TypeOf()和reflect.ValueOf()方法获取的都永远是interface的具体类型,为什么还会出现interface类型呢?答案在下面揭晓。
3 Type的常用操作
上面我们介绍了Type接口的Name()、String()、Kind()三个方法,下面介绍Type其他的一些常用方法,Type的能量更多的体现在下面介绍的方法。首先把当前Type的方法都列举出来,如下所示:
type Type interface {
Align() int
FieldAlign() 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
Len() int
Bits() int
ChanDir() ChanDir
IsVariadic() bool
// 元素类型
Key() Type
Elem() Type
// 成员字段
NumField() int
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 方法
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
// 函数
In(i int) Type
NumIn() int
NumOut() int
Out(i int) Type
common() *rtype
uncommon() *uncommonType
}
3.1 Len()
对于数组(Array)类型,可以通过Len()方法获取数组的大小
3.2 Key()、Elem()
对于数组(Array)、切片(Slice)、指针(Pointer)、map、通道(channel)等聚合或引用类型,可以通过Elem()方法获取对应元素的类型,比如,*string、[]string、map[int]string类型的Elem()方法返回的都是string类型。对于map类型,还可以通过Key()方法获取map key的数据类型,比如map[int]string类型的Key()方法返回的是int类型
同时,Elem()方法获取的是元素的静态类型,如果元素是接口(interface)类型,那么Elem()方法返回的Type是interface,相应的Kind值也是interface。举一个常见的例子:
type Work interface {
DoWork()
doMoreWork()
}
func (e Engineer) DoWork() {
fmt.Printf("Hello, %v", e.Name)
}
func (e Engineer) doMoreWork() {
fmt.Printf("Hello, %v", e.Name)
}
func main() {
// 空接口
var vEmptyInterface interface{} = 1
elemType := reflect.TypeOf(&vEmptyInterface).Elem()
fmt.Printf("vEmptyInterface element stringer: %s, Kind: %s\n",elemType.String(), elemType.Kind())
// 空接口
var vSlice = []interface{}{1, "2"}
elemType = reflect.TypeOf(vSlice).Elem()
fmt.Printf("vSlice element stringer: %s, Kind: %s\n",elemType.String(), elemType.Kind())
// 接口
var vInterface Work = Engineer{"Engineer"}
elemType = reflect.TypeOf(&vInterface).Elem()
fmt.Printf("vInterface element stringer: %s, Kind: %s\n",elemType.String(), elemType.Kind())
}
输出:
vEmptyInterface element stringer: interface {}, Kind: interface
vSlice element stringer: interface {}, Kind: interface
vInterface element stringer: main.Work, Kind: interface
3.3 NumField()、Field()、FieldByIndex()、FieldByName()、FieldByNameFunc()
对于结构体(struct)类型,可以通过NumField()获取结构的成员字段的个数,通过Field()、FieldByIndex()、FieldByName()、FieldByNameFunc()方法获取指定成员字段的字段类型。举个例子:
type Engineer struct {
Name string `json:"name"`
}
func main() {
var x Engineer
x.Name = "John"
tp := reflect.TypeOf(x)
// 字段的数量
fmt.Printf("field number of Engineer: %d\n", tp.NumField())
fieldType := tp.Field(0)
// name字段的名字
fmt.Printf("field name of Engineer: %s\n", fieldType.Name)
}
输出为:
field number of Engineer: 1
field name of Engineer: Name
其实,Type的Field()系列方法返回的不是一个Type接口,而是一个StructField接口,它的定义为:
// A StructField describes a single field in a struct.
type StructField struct {
// Name is the field name.
Name string
// PkgPath is the package path that qualifies a lower case (unexported)
// field name. It is empty for upper case (exported) field names.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers
PkgPath string
Type Type // field type
Tag StructTag // field tag string
Offset uintptr // offset within struct, in bytes
Index []int // index sequence for Type.FieldByIndex
Anonymous bool // is an embedded field
}
可见,StructField接口的一个重要功能是可以获取字段的Tag信息,StructTag接口。并可以通过StructTag接口的Get()、Lookup()方法获取指定Tag的值。接上面的例子,查看Engineer结构体Name字段的Tag:
// 字段添加了json tag,没有添加xml tag
jsonTagValue := fieldType.Tag.Get("json")
xmlTagValue, xmlTagExisted := fieldType.Tag.Lookup("xml")
fmt.Printf("jsonTagValue: %s, xmlTagExisted: %t, xmlTagValue: %s\n", jsonTagValue, xmlTagExisted, xmlTagValue)
上面代码输出:
jsonTagValue: name, xmlTagExisted: false, xmlTagValue:
3.4 NumMethod()、Method()、MethodByName()
对于非接口类型(典型的如结构体),NumMethod()返回的是导出方法的个数。对于接口类型,返回的是导出和非导出接口的中个数。Method()和MethodByName()方法则返回类型的指定方法。Method()和MethodByName()返回的是Method接口,Method接口的定义如下:
// Method represents a single method.
type Method struct {
// Name is the method name.
Name string
// PkgPath is the package path that qualifies a lower case (unexported)
// method name. It is empty for upper case (exported) method names.
// The combination of PkgPath and Name uniquely identifies a method
// in a method set.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers
PkgPath string
Type Type // method type
Func Value // func with receiver as first argument
Index int // index for Type.Method
}
3.5 NumIn()、In()、NumOut()、Out()
如果类型是函数,分别可以通过NumIn()、In()、NumOut()、Out()获取函数的输入参数个数、指定输入参数类型、输出参数个数、指定输出参数类型
4 Value的常用操作
Value与Type类似,除了上面提到的String()、Type()、Interface()、Kind()提供了很多方法查看和操作变量的值。
4.1 IsValid()、IsZero()、IsNil()
IsValid()方法可以判断Value是否有效,只有有效的Value,才能调用Value的相应方法,否则会panic。一般值的Value都是有效的,但是没有指定具体类型的接口是invalid的。例子:
var work Work
v = reflect.ValueOf(work)
fmt.Printf("%t\n", v.IsValid()) // 输出为false
v.isZero() // 会panic
IsZero()用来判断Value是不是相应数据类型的零值,例如:
var vPtr *int
v := reflect.ValueOf(vPtr)
fmt.Printf("%t\n", v.IsZero()) // 输出为true
var vInt int
v = reflect.ValueOf(vInt)
fmt.Printf("%t\n", v.IsZero()) // 输出为true
var vChan []int
v = reflect.ValueOf(vChan)
fmt.Printf("%t\n", v.IsZero()) // 输出为true
当数据类型是chan、func、interface、map、pointer或slice这些可以取nil值的类型时,可以用IsNil()方法来判断Value是不是nil值。例如:
var vPtr *int
v := reflect.ValueOf(vPtr)
fmt.Printf("%t\n", v.IsNil()) // 输出为true
4.2 Can()系列方法
Value提供了一系列方法来检测Value是否具备某项能力的方法。只有Value具备相应的能力时,才能执行相应的功能。如CanInt()返回true,才能调用Int()方法提取Value值为int,否则会panic。只有CanAddr()、CanSet()方法返回结果为true,才能设置Value的值。
我们通常需要使用Value来设置变量的值,所以Value是否是可寻址的(addressable)很重要。那么哪些value是可寻址的呢?一般来说,数组的元素、slice的元素、指针的解引用、可寻址的结构体的所有成员字段。可寻址的结构体的所有成员字段是可寻址的,这是一个很重要的结论,意味着只要一个结构体是可以寻址,那么不管它的成员是什么数据类型,也不管是递归了多少层的孙子成员,都是可寻址的,都是可以修改的。举几个例子:
var vInt int
fmt.Printf("%t\n", reflect.ValueOf(vInt).CanAddr()) // 输出false
fmt.Printf("%t\n", reflect.ValueOf(&vInt).CanAddr()) // 输出false
fmt.Printf("%t\n", reflect.ValueOf(&vInt).Elem().CanAddr()) // 输出true
从上面的例子可以看出,vInt、vInt的指针都是不可寻址的。但是vInt的指针指向的值是可以寻址的。这是因为golang是值传递,reflect.ValueOf(vInt)、reflect.ValueOf(&vInt)的Value存储的仅是变量的值,变量的指针值,但是通过Elem()解引用后的Value存储的变量存储空间位置信息,所以是可寻址的。仔细理解这一点,对正确使用Value来说非常关键
4.3 查看Value的值
如果Value对应的数据类型是基本数据类型,可以通过Int()、UInt()、Float()等方法获取相应的值
如果Value是指针、接口,可以通过Elem()方法分别获取指针指向的变量,接口的具体值
如果Value是数组、切片、map、字符串、通道或数组的指针,可以通过Len()方法获取元素的长度;如果是数组、切片、通道或数组的指针,还可以通过Cap()方法获取容量值
如果Value是数组、切片、字符串,可以通过Index()获取相应元素的Value
如果Value是map,可以通过MapKeys()、MapIndex()分别获取map的key-value映射的Value
如果Value是结构体,可以通过NumberField()获取结构体成员的个数,Field()、FieldByIndex()、FieldByName()获取指定的成员
我们可以通过NumberMethod()方法获取Value的方法数量,Method()、MethodByName()获取Value的指定方法
4.4 创建Value
reflect包提供了一系列方法创建Value,New()、MakeSlice()、MakeMap()、MakeChan()等方法用法与基本golang编程语法类型,只是参数是Type,结果也是用Value表示的
4.5 修改Value
如果Value是可寻址(CanAddr()返回true)、可以设置的(CanSet()返回true),我们可以使用Set()方法设置Value的值。如果是int、bool、string这些基本类型,还可以通过SetInt()、SetBool()、SetString()等相应的特定方法进行设置
如果是通道类型,可以通过Send()、Recv()等方法进行发送和接收消息
4.6 调用函数或方法
可以通过Call()方法调用函数,举例如下:
func add(a, b int) int {
return a + b
}
func main() {
funcValue := reflect.ValueOf(add)
// 函数参数
args := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
// 调用函数
results := funcValue.Call(args)
// 获取结果
fmt.Println(results[0].Int()) // 输出30
}
调用方法的例子:
type Foo int
func (f Foo) Add(a, b int) int {
return a + b
}
func main() {
var foo Foo = 1
v := reflect.ValueOf(foo)
// 获取方法Value
funcValue := v.MethodByName("Add")
// 函数参数
args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
// 调用函数
results := funcValue.Call(args)
// 获取结果
fmt.Println(results[0].Int()) // 输出3
}