反射()是在 Java 出现后迅速流行起来的一种概念,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。
大多数现代的高级语言都以各种形式支持反射功能,反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射。
下面我们就来将介绍一下反射在Go语言中的具体体现以及反射的基本使用方法。
反射的基本概念Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
C/C++语言没有支持反射功能,只能通过 提供非常弱化的程序运行时类型信息;Java、C# 等语言都支持完整的反射功能;Lua、 类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。
Go语言程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。
Go语言提供了 包来访问程序的反射信息。
包Go语言中的反射是由 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 .Type 和 .Value 两部分组成,并且 包提供了 . 和 . 两个函数来获取任意对象的 Value 和 Type。
反射的类型对象(.Type)在Go语言程序中,使用 .() 函数可以获得任意值的类型对象(.Type),程序通过类型对象可以访问任意值的类型信息,下面通过示例来理解获取类型对象的过程:
package main import ( "fmt" "reflect" ) func main() { var a int typeOfA := reflect.TypeOf(a) fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
运行结果如下:
int int
代码说明如下:
反射的类型(Type)与种类(Kind)在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
1) 反射种类(Kind)的定义Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、、bool、 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A {} 定义结构体时,A 就是 {} 的类型。
种类(Kind)指的是对象归属的品种,在 包中有如下定义:
type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针 )
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A {} 定义的结构体属于 种类,*A 属于 Ptr。
2) 从类型对象中获取类型名称和种类Go语言中的类型名称对应的反射获取方法是 .Type 中的 Name() 方法,返回表示类型名称的字符串;类型归属的种类(Kind)使用的是 .Type 中的 Kind() 方法,返回 .Kind 类型的常量。
下面的代码中会对常量和结构体进行类型信息获取。
package main import ( "fmt" "reflect" ) // 定义一个Enum类型 type Enum int const ( Zero Enum = 0 ) func main() { // 声明一个空结构体 type cat struct { } // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(cat{}) // 显示反射类型对象的名称和种类 fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // 获取Zero常量的反射类型对象 typeOfA := reflect.TypeOf(Zero) // 显示反射类型对象的名称和种类 fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
运行结果如下:
cat
Enum int
代码说明如下:
指针与指针指向的元素Go语言程序中对指针获取反射对象时,可以通过 .Elem() 方法获取这个指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作,代码如下:
package main import ( "fmt" "reflect" ) func main() { // 声明一个空结构体 type cat struct { } // 创建cat的实例 ins := &cat{} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(ins) // 显示反射类型对象的名称和种类 fmt.Printf("name:'%v' kind:'%v'n", typeOfCat.Name(), typeOfCat.Kind()) // 取类型的元素 typeOfCat = typeOfCat.Elem() // 显示反射类型对象的名称和种类 fmt.Printf("element name: '%v', element kind: '%v'n", typeOfCat.Name(), typeOfCat.Kind()) }
运行结果如下:
name:'' kind:'ptr'
name: 'cat', kind: ''
代码说明如下:
使用反射获取结构体的成员类型任意值通过 .() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象 .Type 的 () 和 Field() 方法获得结构体成员的详细信息。
与成员获取相关的 .Type 的方法如下表所示。
结构体成员访问的方法列表方法说明
Field(i int)
根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机
() int
返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机
(name ) (, bool)
根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机
(index []int)
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机
(match func() bool) (,bool)
根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机
1) 结构体字段类型.Type 的 Field() 方法返回 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签()等,而且还可以通过 的 Type 字段进一步获取结构体成员的类型信息。
的结构如下:
type StructField struct { Name string // 字段名 PkgPath string // 字段路径 Type Type // 字段反射类型对象 Tag StructTag // 字段的结构体标签 Offset uintptr // 字段在结构体中的相对偏移 Index []int // Type.FieldByIndex中的返回的索引值 Anonymous bool // 是否为匿名字段 }
字段说明如下:
2) 获取成员反射信息下面代码中,实例化一个结构体并遍历其结构体成员,再通过 .Type 的 () 方法查找结构体中指定名称的字段,直接获取其类型信息。
反射访问结构体成员类型及信息:
package main import ( "fmt" "reflect" ) func main() { // 声明一个空结构体 type cat struct { Name string // 带有结构体tag的字段 Type int `json:"type" id:"100"` } // 创建cat的实例 ins := cat{Name: "mimi", Type: 1} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(ins) // 遍历结构体所有成员 for i := 0; i < typeOfCat.NumField(); i++ { // 获取每个成员的结构体字段类型 fieldType := typeOfCat.Field(i) // 输出成员名和tag fmt.Printf("name: %v tag: '%v'n", fieldType.Name, fieldType.Tag) } // 通过字段名, 找到字段类型信息 if catType, ok := typeOfCat.FieldByName("Type"); ok { // 从tag中取出需要的tag fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id")) } }
代码输出如下:
name: Name tag: ''
name: Type tag: 'json:”type” id:”100″'
type 100
代码说明如下:
结构体标签( Tag)通过 .Type 获取结构体成员信息 . 结构中的 Tag 被称为结构体标签()。结构体标签是对结构体字段的额外信息标签。
JSON、BSON 等格式进行序列化及对象关系映射( ,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
1) 结构体标签的格式Tag 在结构体字段后方书写的格式如下:
`key1:”” key2:””`
结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。
2) 从结构体标签中获取值 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:
3) 结构体标签格式错误导致的问题编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,示例代码如下:
package main import ( "fmt" "reflect" ) func main() { type cat struct { Name string Type int `json: "type" id:"100"` } typeOfCat := reflect.TypeOf(cat{}) if catType, ok := typeOfCat.FieldByName("Type"); ok { fmt.Println(catType.Tag.Get("json")) } }
运行上面的代码会输出一个空字符串,并不会输出期望的 type。
代码第 11 行中,在 json: 和 “type” 之间增加了一个空格,这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。所以将第 12 行代码修改为下面的样子,则可以正常打印。
type cat struct { Name string Type int `json:"type" id:"100"` }
运行结果如下:
type