反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在程序运行时,可能会应该访问隔离的设计,导致无法通过变量名作为句柄任意获取到相应的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如:结构体信息、字段名称、类型信息等整合到可执行文件中,并给程序提供接口来访问这些反射信息,这样就可以在程序运行期间,在任意位置通过这些公共的接口来获取到变量的反射信息,并且有能力修改它们。
强类型语言在编译期间会对对象(变量)进行类型,接口,字段,方法等合法性检测,反射技术则允许将对需要调用的对象的信息检查工作从编译期间推迟到运行期间再现场执行。这样一来,就可以在编译期间先不明确目标对象的接口名称、字段(Fields,即对象的成员变量)、可用方法,然后在运行期间根据目标对象自身的信息决定如何处理。它还允许根据判断结果进行实例化新对象和相关方法的调用。
反射主要用途就是使给定的程序能够动态地适应不同的运行情况。利用面向对象建模中的多态性也可以简化编写分别适用于多种不同情形的功能代码,但是反射可以解决多态性并不适用的更普遍情形,从而更大程度地避免硬编码(即把代码的细节 “写死”,缺乏灵活性)的代码风格。
在 Golang 中经常会遇见这样的问题:一个变量的变量名是小写的,意味着不可被导出,但这个变量的值可能需要被转换为指定的数据格式并与外部系统进行交互,例如:转成 JSON 格式,存储到 MongoDB 或 Redis 等数据库。这时就需要一种机制,可以让程序在运行期间简易的访问到这些不可被导出的变量并灵活的对其数据(类型、值)进行操作。显然,这就是反射机制的应用场景。
在 Golang 中,反射还具有以下应用场景:
...interface{}
来传递参数,大大减少了代码行数。type IT interface {
test1()
}
type T struct {
A string
}
func (t *T) test1() {}
func main() {
t := &T{}
ITF := reflect.TypeOf((*IT)(nil)).Elem()
tv := reflect.TypeOf(t)
fmt.Println(tv.Implements(ITF))
}
Golang 内建的 reflect 包实现了运行时反射。典型用法是用静态类型 interface{} 保存一个值,通过调用 TypeOf 函数获取其动态类型信息,该函数返回一个 Type 类型值。调用 ValueOf 函数返回一个 Value 类型值,代表运行时的数据。Zero 接受一个 Type 类型参数并返回一个代表该类型零值的 Value 类型值。
Go 程序的反射机制无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息,常见的就是 Struct Tags。
使用 reflect.TypeOf() 函数可以获得任意对象的 reflect.Type(反射类型对象),程序通过 reflect.Type 可以访问对象的类型信息。
示例:
package main
import (
"fmt"
"reflect"
)
type Enum int
const (
Zero Enum = 0
)
type Student struct {
Name string
Age int
}
func main() {
var stu Student
typeOfStu := reflect.TypeOf(stu)
fmt.Println("类型名称: ", typeOfStu.Name())
fmt.Println("种类: ", typeOfStu.Kind())
typeOfZero := reflect.TypeOf(Zero)
fmt.Println("类型名称: ", typeOfZero.Name())
fmt.Println("种类: ", typeOfZero.Kind())
}
结果:
$ go run tst.go
类型名称: Student
种类: struct
类型名称: Enum
种类: int
需要注意的是,这里的 Kind(种类)和 Type(类型)是两个概念。反射种类(Kind)的定义:Go 程序中的 Type 指的是系统原生数据类型,如:int、string、bool、float32 等,以及使用 type 关键字自定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。而 Kind 指的是对象归属的品种。
编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如,需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
通过 reflect.Elem() 方法获取指针指向的元素类型。这个获取过程被称为 “取元素”,等效于对指针类型变量执行了一次 *
操作。
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
}
func main() {
var stu = &Student{Name: "kitty", Age: 20}
typeOfStu := reflect.TypeOf(stu)
fmt.Printf("name: '%v', kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind())
typeOfStu = typeOfStu.Elem()
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind())
}
结果:
$ go run tst.go
name: '', kind: 'ptr'
element name: 'Student', element kind: 'struct'
注意:指针变量的类型名称是空,不是 *Student。
任意对象通过 reflect.TypeOf() 获得反射对象的类型信息后,如果它的类型是结构体,可以通过 reflect.Type(反射类型对象)的 NumField() 和 Field() 方法获得结构体成员的详细信息。
其中,Field() 方法返回 StructField 结构,用于描述目标结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如:偏移、索引、是否为匿名字段、结构体标签(Struct Tags)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。StructField 的声明如下:
type StructField struct {
Name string // 字段名
PkgPath string // 字段路径
Type Type // 字段反射类型对象
Tag StructTag // 字段的结构体标签
Offset uintptr // 字段在结构体中的相对偏移
Index []int // Type.FieldByIndex 中的返回的索引值
Anonymous bool // 是否为匿名字段
}
示例:下面代码中实例化一个结构体并遍历其成员,再通过 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
package main
import (
"fmt"
"reflect"
)
func main() {
type cat struct {
Name string
Type int `json:"type" id:"100"`
}
ins := cat{Name: "mimi", Type: 1}
typeOfCat := reflect.TypeOf(ins)
for i := 0; i < typeOfCat.NumField(); i++ {
fieldType := typeOfCat.Field(i)
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
if catType, ok := typeOfCat.FieldByName("Type"); ok {
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
}
}
结果:
$ go run tst.go
name: Name tag: ''
name: Type tag: 'json:"type" id:"100"'
type 100
反射不仅可以获取任意对象的类型信息,还可以动态地获取或者设置对象(变量)的值,使用 reflect.Value 获取和设置变量的值。变量、interface{} 和 reflect.Value 是可以相互转换的。这点在实际开发中,会经常碰到。
使用 reflect.ValueOf() 函数获得对象的 reflect.Value(反射值对象)。书写格式如下:
rValue := reflect.ValueOf(rawValue)
返回包含有 rawValue 的值信息。reflect.Value 与原值间可以通过 “值包装” 和 “值获取” 互相转化。reflect.Value 是一些反射操作的重要类型,如:反射调用函数。
通过 reflect.Value 重新获得原始值,可以通过下面几种方法从 reflect.Value 中获取原值。
示例:下面代码中,将整型变量中的值使用 reflect.Value 获取反射值对象。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 1024
valueOfA := reflect.ValueOf(a)
var getA int = valueOfA.Interface().(int)
var getB int = int(valueOfA.Int())
fmt.Println(getA, getB)
}
结果:
$ go run tst.go
1024 1024
其中,将 valueOfA 反射值对象以 interface{} 类型取出,通过类型断言转换为 int 类型并赋值给 getA。
反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意成员的值的访问。
示例:下面代码构造一个结构体包含不同类型的成员。通过 reflect.Value 提供的成员访问函数,可以获得结构体值的各种数据。
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
// 嵌入字段
float32
bool
next *Student
}
func main() {
rValue := reflect.ValueOf(Student{
next: &Student{},
})
fmt.Println("NumField:", rValue.NumField())
floatField := rValue.Field(2)
fmt.Println("Field:", floatField.Type())
fmt.Println("FieldByName(\"Age\").Type:", rValue.FieldByName("Age").Type())
fmt.Println("FieldByIndex([]int{4, 0}).Type():", rValue.FieldByIndex([]int{4, 1}).Type())
}
结果:
$ go run tst.go
NumField: 5
Field: float32
FieldByName("Age").Type: int
FieldByIndex([]int{4, 0}).Type(): int
注:[]int{4,1}
中的 4 表示,在 Student 结构中索引值为 4 的成员,也就是 next。next 的类型为 Student,也是一个结构体,因此使用 []int{4,1}
中的 1 继续在 next 值的基础上索引,结构为 Student 中索引值为 1 的 Age 字段,类型为 int。
注意,当我们通过反射去访问一个变量的值时,我们需要判断反射值是否为空和有效性。反射值对象(reflect.Value)提供一系列方法进行零值和空判定。
下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定。
package main
import (
"fmt"
"reflect"
)
func main() {
// *int 类型空指针变量
var a *int
fmt.Println("var a *int: ", reflect.ValueOf(a).IsNil())
// nil 值
fmt.Println("nil: ", reflect.ValueOf(nil).IsValid())
// *int 类型空指针
fmt.Println("(*int)(nil): ", reflect.ValueOf((*int)(nil)).Elem().IsValid())
s := struct{}{}
// 尝试从结构体中查找一个不存在的字段
fmt.Println("不存在的结构体成员: ", reflect.ValueOf(s).FieldByName("").IsValid())
// 尝试从结构体中查找一个不存在的方法
fmt.Println("不存在的方法: ", reflect.ValueOf(s).MethodByName("").IsValid())
// 实例化一个 map
m := map[int]int{}
// 尝试从 map 中查找一个不存在的键
fmt.Println("不存在的键: ", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
结果:
$ go run tst.go
var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的方法: false
不存在的键: false
注:MapIndex() 方法能根据给定的 reflect.Value 类型的值查找 Map,并且返回查找到的结果。
使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。在已知值的类型时,应尽量使用值对应类型的反射设置值。
值的修改从表面意义上叫可寻址,换一种说法就是值必须 “可被设置”。那么,想修改变量值,一般的步骤是:
可以被寻址,简单地说就是这个变量必须能被修改。示例代码如下:
package main
import "reflect"
func main() {
var a int = 1024
rValue := reflect.ValueOf(a)
// 尝试将 a 修改为 1 (此处会崩溃)
rValue.SetInt(1)
}
错误信息:
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
程序崩溃的原因是:SetInt正在使用一个不能被寻址的值。
从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 1024
rValue := reflect.ValueOf(&a)
// 取出 &a 地址的元素 (a 的值)
rValue = rValue.Elem()
rValue.SetInt(1)
fmt.Println(rValue.Int())
}
注:使用 reflect.Value 类型的 Elem() 方法获取 a 地址的元素,也就是 a 的值。reflect.Value 的 Elem() 方法返回的值类型也是 reflect.Value。此时 rValue 表示的是 a 的值且可以寻址。使用 SetInt() 方法设置值时不再发生崩溃。
另外,当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。
结构体成员中,如果字段(Field)可被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
package main
import "reflect"
func main() {
type dog struct {
legCount int
}
valueOfDog := reflect.ValueOf(&dog{})
valueOfDog = valueOfDog.Elem()
vLegCount := valueOfDog.FieldByName("legCount")
// 尝试设置 legCount 的值 (这里会发生崩溃)
vLegCount.SetInt(4)
}
错误信息:
panic: reflect: reflect.flag.mustBeAssignable using value obtained using unexported field
原因是:SetInt() 使用的值来自于一个未导出的字段。
为了能修改这个值,需要将该字段导出。将 dog 中的 legCount 的成员首字母大写,导出 LegCount 让反射可以访问,修改后的代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
type dog struct {
LegCount int
}
valueOfDog := reflect.ValueOf(&dog{})
valueOfDog = valueOfDog.Elem()
vLegCount := valueOfDog.FieldByName("LegCount")
vLegCount.SetInt(4)
fmt.Println(vLegCount.Int())
}
所以在 json 包中,对结构体成员名为小写的字段不进行处理。
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即 *int,代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
typeOfA := reflect.TypeOf(a)
// 根据反射类型对象创建类型实例
aIns := reflect.New(typeOfA)
fmt.Println(aIns.Type(), aIns.Kind())
}
结果:
$ go run tst.go
*int ptr
注:使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。
如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
package main
import (
"fmt"
"reflect"
)
func add(a, b int) int {
return a + b
}
func main() {
// 将函数包装为反射值对象
funcValue := reflect.ValueOf(add)
// 构造函数参数,传入两个整形值
paramList := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}
// 反射调用函数
retList := funcValue.Call(paramList)
fmt.Println(retList[0].Int())
}
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
调用方法和调用函数是一样的,只不过结构体需要先通过 rValue.Method() 先获取方法再调用。
package main
import (
"fmt"
"reflect"
)
type MyMath struct {
Pi float64
}
func (myMath MyMath) Sum(a, b int) int {
return a + b
}
func (myMath MyMath) Dec(a, b int) int {
return a - b
}
func main() {
var myMath = MyMath{Pi: 3.14159}
// 获取 myMath 的值对象
rValue := reflect.ValueOf(myMath)
// 获取到该结构体有多少个方法
//numOfMethod := rValue.NumMethod()
// 构造函数参数,传入两个整形值
paramList := []reflect.Value{reflect.ValueOf(30), reflect.ValueOf(20)}
// 调用结构体的第一个方法 Method(0)
result := rValue.Method(0).Call(paramList)
fmt.Println(result[0].Int())
}
注:在反射值对象中方法索引的顺序并不是结构体方法定义的先后顺序,而是根据方法的 ASCII 码值来从小到大排序,所以 Dec 排在第一个,也就是 Method(0)。
Struct Tags 实现了 Golang 的反射(Reflect)机制,通过 Tags 将 Struct 的信息记录下来,并提供 reflect 包来调用这些接口,使得在程序运行的过程中,程序自身可以通过这些接口(reflect 包)来获得这些信息并实现预期的逻辑。例如:在需要将 Struct 转换为其它数据格式时,会根据 Tags 中定义的信息进行转换。
Struct Tags 类似注释,使用反引号 “`”,Golang 提供了 reflect 包来对 Struct Tags 进行解析。
示例:
package main
import (
"fmt"
"reflect"
)
type employee struct {
ID int `json:"id"`
Name string `json:"名字" validate:"presence,min=2,max=40"`
Age int `json:"年龄"`
Desc string `json:"描述" back:"好看否"`
weight float64 `json:"weight" 单位:"kg"`
Salary float64 `json:"-"`
Email string `validate:"email,required"`
MateName string `json:"mate_name,omitempty"`
}
func main() {
zhangsan := employee{
ID: 1,
Name: "张三",
Age: 18,
Desc: "秀色可餐",
weight: 48.0,
Salary: 12.0,
MateName: "Prince",
}
t := reflect.TypeOf(zhangsan)
fmt.Println("Type: ", t.Name())
fmt.Println("Kind: ", t.Kind())
fmt.Println("Num: ", t.NumField())
tagName := "validate"
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get(tagName)
fmt.Printf("%d. %v(%v), tag:'%v'\n", i+1, field.Name, field.Type.Name(), tag)
}
}
结果:
Type: employee
Kind: struct
Num: 8
1. ID(int), tag:''
2. Name(string), tag:'presence,min=2,max=40'
3. Age(int), tag:''
4. Desc(string), tag:''
5. weight(float64), tag:''
6. Salary(float64), tag:''
7. Email(string), tag:'email,required'
8. MateName(string), tag:''
通过 reflect 包,程序能够获取结构体的基本信息,包括它的成员清单、成员名称和成员类型。调用 field.Tag.Get
方法可以返回与 tagName 名匹配的 Struct Tags 的字符串,基于此我们就可以自由地去实现想要的逻辑了。
例如 json.Marshal
方法就有使用到反射机制,来完成 Struct 和 JSON 之间的数据格式转换。
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type employee struct {
ID int `json:"id"`
Name string `json:"名字" validate:"presence,min=2,max=40"`
Age int `json:"年龄"`
Desc string `json:"描述" back:"好看否"`
weight float64 `json:"weight" 单位:"kg"`
Salary float64 `json:"-"`
Email string `validate:"email,required"`
MateName string `json:"mate_name,omitempty"`
}
func main() {
zhangsan := employee{
ID: 1,
Name: "张三",
Age: 18,
Desc: "秀色可餐",
weight: 48.0,
Salary: 12.0,
MateName: "Prince",
}
fmt.Println(zhangsan)
re, _ := json.Marshal(zhangsan)
fmt.Println(string(re))
}
结果:
$ go run tst.go
{1 张三 18 秀色可餐 48 12 Prince}
{"id":1,"名字":"张三","年龄":18,"描述":"秀色可餐","Email":"","mate_name":"Prince"}
其中,有以下要点需要注意:
json:"XXX"
,则 XXX 作为 JSON key。json:"XXX"
,则 JSON key 和 Struct 成员名保持一致。json:"-"
,则不进行转换。json:",omitempty"
,则表示当成员数值为空时,可直接忽略。可见,在 json 包中,Struct Tag json:"XXX"
是用来指导 json.Marshal/Unmarshal 的。
此外,我们还可以通过 Struct Tags 中的其他 Tag 类型来实现别的功能,例如:限定成员 Age 的值在 1-100 之间。
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age" valid:"1-100"`
}
func (p *Person) validation() bool {
v := reflect.ValueOf(*p)
tag := v.Type().Field(1).Tag.Get("valid")
val := v.Field(1).Interface().(int)
fmt.Printf("tag=%v, value=%v\n", tag, val)
result := strings.Split(tag, "-")
var min, max int
min, _ = strconv.Atoi(result[0])
max, _ = strconv.Atoi(result[1])
if val >= min && val <= max {
return true
}
return false
}
func main() {
person1 := Person{"tom", 12}
if person1.validation() {
fmt.Printf("person 1: valid\n")
} else {
fmt.Printf("person 1: invalid\n")
}
person2 := Person{"tom", 250}
if person2.validation() {
fmt.Printf("person 2 valid\n")
} else {
fmt.Printf("person 2 invalid\n")
}
}
结果:
$ go run tst.go
tag=1-100, value=12
person 1: valid
tag=1-100, value=250
person 2 invalid
上述例子我们给 Person 实现了一个 validate 方法,用于验证 Age 的值是否合理。我们再将这个函数扩展至验证任意结构体的 Age 成员。
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age" valid:"1-100"`
}
func validateStruct(s interface{}) bool {
v := reflect.ValueOf(s)
for i := 0; i < v.NumField(); i++ {
fieldTag := v.Type().Field(i).Tag.Get("valid")
fieldName := v.Type().Field(i).Name
fieldType := v.Field(i).Type()
fieldValue := v.Field(i).Interface()
if fieldTag == "" || fieldTag == "-" {
continue
}
if fieldName == "Age" && fieldType.String() == "int" {
val := fieldValue.(int)
tmp := strings.Split(fieldTag, "-")
var min, max int
min, _ = strconv.Atoi(tmp[0])
max, _ = strconv.Atoi(tmp[1])
if val >= min && val <= max {
return true
}
return false
}
}
return true
}
func main() {
person1 := Person{"tom", 12}
if validateStruct(person1) {
fmt.Printf("person 1: valid\n")
} else {
fmt.Printf("person 1: invalid\n")
}
person2 := Person{"tom", 250}
if validateStruct(person2) {
fmt.Printf("person 2: valid\n")
} else {
fmt.Printf("person 2: invalid\n")
}
}
结果:
$ go run tst.go
person 1: valid
person 2: invalid
上述例子中的 validateStruct 函数可以接收任意类型的变量作为参数(interface{}
作为所有类型的 “基类”)并进行校验。