一、反射的基本概念
1.1 反射简介
反射 是指在程序运行期对程序本身进行访问和修改的能力。是一种在运行时动态地获取和操作变量的类型和值的机制,可以大大增强代码的灵活性和通用性。
程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go 程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。
Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
Go语言中的反射是由 reflect 包提供支持的,程序在运行期使用reflect包访问程序的反射信息,它定义了两个重要的类型 Type 和 Value, 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
反射的核心 是 Type 和 Value,Type 表示变量的类型,Value 表示变量的值。
二、反射的类型(reflect.Type)对象 和 值(reflect.Value)对象
2.1 反射的类型(Type)与种类(Kind)
在使用反射时,首先需要理解类型(Type)和种类(Kind)的区别。
类型(Type) 指的是系统原生数据类型(具体类型),如 bool、int、*int、uint、[2]]uint, []uint、uintptr、float32、string、chan int、 map[int]int、func(int) int、及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{}
定义结构体时,A 就是 struct{} 的类型。
种类(Kind) 指的是对象归属的品种(一类),如 所有类型的指针类型都属于 ptr 种类、所有的数组都属于 array种类、所有的切片都属于 slice 种类、所有的 map[keytype]valuetype 都属于 map 种类、所有的结构体都属于 struct 种类、所有的接口类型都属于 Interface 种类、int类型属于 int种类、string类型属于 string种类等,在 reflect 包中 Kind 有如下定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// src/reflect/type.go
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型, bool
Int // 有符号整型, int
Int8 // 有符号8位整型,int8
Int16 // 有符号16位整型,int16
Int32 // 有符号32位整型,int32
Int64 // 有符号64位整型,int64
Uint // 无符号整型,uint
Uint8 // 无符号8位整型,uint8
Uint16 // 无符号16位整型,uint16
Uint32 // 无符号32位整型,uint32
Uint64 // 无符号64位整型,uint64
Uintptr // 指针,泛指所有类型的指针,如: *int、*int16、*uint、*float、*string 等等
Float32 // 单精度浮点数,float32
Float64 // 双精度浮点数,float64
Complex64 // 64位复数类型,complex64
Complex128 // 128位复数类型,complex128
Array // 数组,泛指所有类型的数组,如: [2]int, [50]int, [10]uint16, [6]string, [8]struct{} 等等
Chan // 通道,泛指所有类型的chan,如: chan int, chan string, chan struct{} 等等
Func // 函数
Interface // 接口
Map // 映射
Pointer // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
|
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 ptr。
Tips: 编程中,使用最多的是类型(Type),但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
type Person struct {
Name string
Age int
}
func main() {
var b bool = true
var i int = 1
var i16 int16 = 16
var p_i *int = &i
var ui uint = 10
var ui32 uint32 = 32
var up uintptr
var fl float64 = 3.14
var arr [3]int
var ch chan int
var fn func()
var ifc interface{} = &i
var mp map[string]int
var sl []int
var s string = "hello"
var st Person
v_b := reflect.TypeOf(b)
v_i := reflect.TypeOf(i)
v_p_i := reflect.TypeOf(p_i)
v_i16 := reflect.TypeOf(i16)
v_ui := reflect.TypeOf(ui)
v_ui32 := reflect.TypeOf(ui32)
v_up := reflect.TypeOf(up)
v_fl := reflect.TypeOf(fl)
v_arr := reflect.TypeOf(arr)
v_ch := reflect.TypeOf(ch)
v_fn := reflect.TypeOf(fn)
v_ifc := reflect.TypeOf(ifc)
v_mp := reflect.TypeOf(mp)
v_sl := reflect.TypeOf(sl)
v_s := reflect.TypeOf(s)
v_st := reflect.TypeOf(st)
fmt.Printf("%v, %v, %v\n", v_b, v_b.Name(), v_b.Kind())
fmt.Printf("%v, %v, %v\n", v_i, v_i.Name(), v_i.Kind())
fmt.Printf("%v, %v, %v\n", v_p_i, v_p_i.Name(), v_p_i.Kind())
fmt.Printf("%v, %v, %v\n", v_i16, v_i16.Name(), v_i16.Kind())
fmt.Printf("%v, %v, %v\n", v_ui, v_ui.Name(), v_ui.Kind())
fmt.Printf("%v, %v, %v\n", v_ui32, v_ui.Name(), v_ui32.Kind())
fmt.Printf("%v, %v, %v\n", v_up, v_up.Name(), v_up.Kind())
fmt.Printf("%v, %v, %v\n", v_fl, v_fl.Name(), v_fl.Kind())
fmt.Printf("%v, %v, %v\n", v_arr, v_arr.Name(), v_arr.Kind())
fmt.Printf("%v, %v, %v\n", v_ch, v_ch.Name(), v_ch.Kind())
fmt.Printf("%v, %v, %v\n", v_fn, v_fn.Name(), v_fn.Kind())
fmt.Printf("%v, %v, %v\n", v_ifc, v_ifc.Name(), v_ifc.Kind())
fmt.Printf("%v, %v, %v\n", v_mp, v_mp.Name(), v_mp.Kind())
fmt.Printf("%v, %v, %v\n", v_sl, v_sl.Name(), v_sl.Kind())
fmt.Printf("%v, %v, %v\n", v_s, v_s.Name(), v_s.Kind())
fmt.Printf("%v, %v, %v\n", v_st, v_st.Name(), v_st.Kind())
}
|
2.2 Go语言reflect.ValueOf()和reflect.Value(通过反射获取值信息)
反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用 reflect.Value 获取和设置变量的值。
将一个接口值传递给一个 reflect.ValueOf 函数调用时,此调用返回的是代表着此接口值的动态值的一个 reflect.Value 值。必须通过间接的途径获得一个代表一个接口值的 reflect.Value 值。
reflect.Value 类型有很多方法(https://golang.google.cn/pkg/reflect/)。可以调用这些方法来观察和操纵一个 reflect.Value 属主值表示的 Go 值。这些方法中的有些适用于所有种类类型的值,有些只适用于一种或几种类型的值。
通过不合适的 reflect.Value 属主值调用某个方法将在运行时产生一个恐慌(panic)。
一个 reflect.Value 值的 CanSet 方法将返回此 reflect.Value 值代表的 Go 值是否可以被修改(可以被赋值)。如果一个 Go 值可以被修改,则可以调用对应的 reflect.Value 值的 Set 方法来修改此 Go 值。注意:reflect.ValueOf 函数直接返回的 reflect.Value 值都是不可修改的。
reflect.ValueOf 返回 reflect.Value 类型,包含有 原值 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化。
reflect.Value 是一些反射操作的重要类型,如反射调用函数。
从反射值对象获取被包装的值
Go语言中可以通过 reflect.Value 重新获得原始值。
- 从反射值对象(reflect.Value)中获取值的方法
可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示。
方法名 |
说明 |
Interface() interface{} |
将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 |
将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 |
将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 |
将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool |
将值以 bool 类型返回 |
Bytes() []bytes |
将值以字节数组 []bytes 类型返回 |
String() string |
将值以字符串类型返回 |
2) 从反射值对象(reflect.Value)中获取值的例子 |
|
下面代码中,将整型变量中的值使用 reflect.Value 获取反射值对象(reflect.Value)。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。 |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明整型变量a并赋初值
var a int = 1024
// 获取变量a的反射值对象
valueOfA := reflect.ValueOf(a)
// 获取interface{}类型的值, 通过类型断言转换
var getA int = valueOfA.Interface().(int)
// 获取64位的值, 强制类型转换为int类型
var getA2 = valueOfA.Int()
fmt.Printf("getA: %T, %#v\n", getA, getA)
fmt.Printf("getA2: %T, %#v\n", getA2, getA2)
}
|
三、反射的功能
3.1 可以使用反射机制获取变量的类型和值
在 Golang 中,可以使用 reflect.TypeOf() 函数获取变量的 reflect.Type,程序通过类型对象可以访问任意变量的类型信息。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
"reflect"
)
// 定义一个Enum类型
type Enum int
const Zero Enum = 0
func main() {
// 声明一个空结构体
type cat struct {
}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(cat{})
// 显示反射类型对象的名称和种类,
// 输出 cat 的类型名称和种类,类型名称就是 cat,而 cat 属于一种结构体种类,因此种类为 struct。
fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // cat struct
// 获取Zero常量的反射类型对象
typeOfA := reflect.TypeOf(Zero)
// 显示反射类型对象的名称和种类
fmt.Println(typeOfA.Name(), typeOfA.Kind()) // Enum int
}
|
在 Golang 中,可以使用 reflect.ValueOf() 函数获取变量的 Value。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
var x int = 1
var y float64 = 3.14
var z bool = true
var s string = "hello"
var p *Person = &Person{"Tom", 18}
// 使用 reflect.TypeOf() 函数获取变量的 Type
fmt.Println(reflect.TypeOf(x)) // 输出为: int
fmt.Println(reflect.TypeOf(y)) // float64
fmt.Println(reflect.TypeOf(z)) // bool
fmt.Println(reflect.TypeOf(s)) // string
fmt.Println(reflect.TypeOf(p)) // *main.Person
// 使用 reflect.ValueOf() 函数获取变量的 Value
fmt.Println(reflect.ValueOf(x)) // 输出为:1
fmt.Println(reflect.ValueOf(y)) // 3.14
fmt.Println(reflect.ValueOf(z)) // true
fmt.Println(reflect.ValueOf(s)) // hello
fmt.Println(reflect.ValueOf(p)) // &{Tom 18}
}
|
- 对指针与指针指向的元素获取反射对象
Go语言的反射中对所有指针变量的种类都是 Ptr,但需要注意的是,指针变量的类型名(Name函数输出)称是空,不是 Type,指针变量的种类(Kind函数输出)为 ptr 。
Go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个操作,这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 创建了 cat 结构体的实例,ins 是一个 *cat 类型的指针变量。
ins := &cat{}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins) // 对指针变量获取反射类型信息。
// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n", typeOfCat.Name(), typeOfCat.Kind()) // name:'' kind:'ptr'
// 取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
typeOfCat = typeOfCat.Elem() // element name: 'cat', element kind: 'struct'
// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}
|
3.2 可以使用反射机制获取结构体变量的成员(字段)的类型和值
任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象 reflect.Type 的 NumField() 和 Field() 方法获得结构体成员的详细信息。
与成员获取相关的 reflect.Type 的方法如下表所示。
方法 |
说明 |
Field(i int) StructField |
根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机, Field() 方法和 NumField 一般都是配对使用,用来实现结构体成员的遍历操作 |
NumField() int |
返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机 |
FieldByName(name string) (StructField, bool) |
根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) StructField |
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) (StructField,bool) |
根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机 |
StructField 描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
StructField 的结构如下:
1
2
3
4
5
6
7
8
9
|
type StructField struct {
Name string // 字段名
PkgPath string // 字段在结构体中的路径。
Type Type // 字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
Tag StructTag // 结构体标签,为结构体字段标签的额外信息,可以单独提取。
Offset uintptr // 字段在结构体中的相对偏移, FieldByIndex 中的索引顺序。
Index []int // Type.FieldByIndex中的返回的索引值
Anonymous bool // 表示该字段是否为匿名字段。
}
|
使用反射获取结构体变量的成员(字段)需要分为2步。
- 使用 reflect.TypeOf 和 reflect.ValueOf() 函数获取结构体的 Type 和 Value
- 使用 Type.FieldByName() 和 Value.FieldByName() 函数获取结构体变量的成员(字段)的 Type 和 Value
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
Type int `json:"type" id:"100"`
Next *Person
}
func main() {
p := Person{"Tom", 18, 2}
typ := reflect.TypeOf(p) // reflect.TypeOf 函数获取结构体变量的 Type : Person
name, _ := typ.FieldByName("Name") // 根据字段名查找结构体字段信息,返回的结构体字段信息类型为 StructField
age, _ := typ.FieldByName("Age") // 根据字段名查找结构体字段信息,返回的结构体字段信息类型为 StructField
value := reflect.ValueOf(p) // reflect.ValueOf() 函数获取结构体变量的 Value
v_name := value.FieldByName("Name") // Value.FieldByName() 函数获取结构体变量的成员(字段)的 Value : Tom
v_age := value.FieldByName("Age") // Value.FieldByName() 函数获取结构体变量的成员(字段)的 Value : 10
fmt.Println(name.Type, v_name) // string Tom
fmt.Println(age.Type, v_age) // int 18
// 遍历结构体所有成员
// 使用 reflect.Type 类型的 NumField() 方法获得结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。
for i := 0; i < typ.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typ.Field(i) // 使用 reflect.Type 的 Field() 方法返回的结构不再是 reflect.Type 而是 StructField 结构体
// 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
// 输出
// name: Name tag: ''
// name: Age tag: ''
// name: Type tag: 'json:"type" id:"100"'
}
// 通过字段名, 找到字段类型信息
if psType, ok := typ.FieldByName("Type"); ok {
// 从tag中取出需要的tag
fmt.Println(psType.Tag.Get("json"), psType.Tag.Get("id")) // 输出: type 100
}
// 根据索引查找值中, next字段的int字段的值
fmt.Println("FieldByIndex([]int{4, 0}).Type()", typ.FieldByIndex([]int{4, 0}).Type())
}
|
3.3 结构体标签(Struct Tag)
通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(Struct Tag)。结构体标签是对结构体字段的额外信息标签。
JSON、BSON、xml、form、yaml、toml 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
1、结构体标签的格式
Tag 在结构体字段后方书写的格式如下:
1
|
`key1:"value1" key2:"value2"`
|
结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。
2、从结构体标签中获取值
Struct Tag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:
方法 |
功能 |
func (tag StructTag) Get(key string) string |
根据 Tag 中的键获取对应的值,例如key1:"value1" key2:"value2" 的 Tag 中,可以传入“key1”获得“value1”。 |
func (tag StructTag) Lookup(key string) (value string, ok bool) |
根据 Tag 中的键获取对应的值,ok用于判断查询值是否存在。 |
3.4 修改结构体变量的成员(字段)的值
在 Golang 中,可以使用反射机制修改结构体变量的成员(字段)的值。结构体中只有可导出的字段是“可设置”的。使用反射修改结构体变量的成员(字段)的值需要分为3步。
- 使用reflect.ValueOf().Elem() 函数链获取结构体的 Value
- 使用 Value.FieldByName() 函数获取结构体的成员变量的 Value
- 使用 Value.SetInt() 或者 Value.SetString() 等函数修改成员变量的值
示例:
1
2
3
4
5
6
7
8
9
10
11
12
|
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Tom", 18}
value := reflect.ValueOf(&p).Elem()
fmt.Println("settability of v:", value.CanSet())
value.FieldByName("Name").SetString("Jerry")
value.FieldByName("Age").SetInt(20)
fmt.Println(p)
}
|
四、Go语言的反射三定律
Go语言的接口都是静态类型,虽然在运行时中,接口变量存储的值也许会变,但接口变量的类型是不会变的。必须精确地了解这些,因为反射与接口是密切相关的。
4.1 反射第一定律:反射可以将 “接口类型变量” 转换为 “反射类型对象”
注:这里反射类型指 reflect.Type 和 reflect.Value。
从使用方法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。
reflect 包的两种类型 Type 和 Value,这两种类型使访问接口内的数据成为可能,它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。当然,从 reflect.Value 也很容易获取到 reflect.Type。
在 reflect.TypeOf 和 reflect.ValueOf 的函数签名里包含一个空接口:
1
2
3
4
5
6
7
|
// 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
// 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
|
调用 reflect.TypeOf(x) 或 reflect.ValueOf(x) 时,x 被存储在一个空接口变量中被传递到函数内,然后函数对空接口变量进行拆解,恢复其类型信息。
类型 reflect.Value 有一个方法 Type(),它会返回一个 reflect.Type 类型的对象。
Type 和 Value 都有一个名为 Kind 的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice 等。
Value 类型有一些类似于 Int、Float、String 的方法,用来提取底层的数据:
Int 方法用来提取 int64
Float 方法用来提取 float64,
String 方法用来提取 string,
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // type: float64
fmt.Println("kind is float64:", v.Kind() == reflect.Float64) // kind is float64: true
fmt.Println("value:", v.Float()) // value: 3.4
var s string = "abc"
v1 := reflect.ValueOf(s)
fmt.Println("type:", v.Type()) // type: string
fmt.Println("kind is string:", v.Kind() == reflect.String) // kind is string: true
fmt.Println("value:", v.String()) // value: abc
}
|
Value 的 getter 和 setter 方法
为了保证 API 的精简,这两个方法操作的是某一组类型范围最大的那个。比如,处理任何含符号整型数,都使用 int64,也就是说 Value 类型的 Int 方法返回值为 int64 类型,SetInt 方法接收的参数类型也是 int64 类型。实际使用时,可能需要转化为实际的类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
"reflect"
)
func main() {
var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // type: uint8
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // kind is uint8: true
x = uint8(v.Uint()) // v.Uint returns a uint64.
}
|
反射对象的 Kind 方法描述的是基础类型,而不是静态类型。如果一个反射对象包含了用户定义类型的值,如下所示:
1
2
3
4
5
6
|
type MyInt int
var xx MyInt = 7
vt := reflect.TypeOf(xx)
fmt.Println("vt Kind:", vt.Name(), vt.Kind()) // vt Kind: MyInt int
vv := reflect.ValueOf(xx)
fmt.Println("vv Kind:", vv.Type().Name(), vv.Kind()) // vv Kind: MyInt int
|
上面的代码中,虽然变量 v 的静态类型是 MyInt,而不是 int,但 Kind 方法仍然会返回 reflect.Int。换句话说 Kind 方法不会像 Type 方法一样区分 MyInt 和 int。
4.2 反射第二定律:反射可以将 “反射类型对象” 转换为 “接口类型变量”
根据一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。
其函数声明如下:
1
2
|
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
|
然后,可以通过断言,恢复底层的具体值:
1
2
3
4
|
type MyInt int
vv := reflect.ValueOf(xx)
vall := vv.Interface().(MyInt)
fmt.Printf("%T, %#v\n", vall, vall) // MyInt, 7
|
标准库中的 fmt.Println 和 fmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包,因此 fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给格式化打印程序.
不需要对 v.Interface() 的结果进行类型断言,空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息。
简单来说 Interface 方法和 ValueOf 函数作用恰好相反,唯一一点是,返回值的静态类型是 interface{}。
Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。
4.3 反射第三定律:如果要修改“反射类型对象”其值必须是“可写的”
下面这段代码虽然不能正常工作,但是非常值得研究:
1
2
3
|
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic
|
如果运行这段代码,它会抛出一个奇怪的异常:
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
这里问题不在于值7.1 不能被寻址,而是因为变量 v 是“不可写的”,“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。
可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”,对于上面的例子,可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
|
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
}
// 运行结果如下:
// settability of v: false
|
对于一个不具有“可写性”的 Value 类型变量,调用 Set 方法会报出错误。
可写性 有些类似于寻址能力,但是更严格,它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个反射对象是否存储了原始值而决定的。
传递给 reflect.ValueOf 函数的是变量的一个拷贝(接口赋值是值传递),而非 变量 本身,想象一下如果v.SetFloat(7.1)这行代码能够成功执行, 它不会更新 x,虽然看起来变量 v 是根据 x 创建的,相反它会更新 x 存在于反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。
如果想通过反射修改变量 x,就要把想要修改的变量的指针传递给反射库。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type()) // type of p: *float64
fmt.Println("settability of p:", p.CanSet()) // settability of p: false
// 反射对象 p 是不可写的,但是我们也不像修改 p,事实上我们要修改的是 *p。为了得到 p 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value 类型对象 v 中:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet()) // settability of v: true
// 由于变量 v 代表 x, 因此我们可以使用 v.SetFloat 修改 x 的值:
v.SetFloat(7.1)
fmt.Println(v.Interface()) // 7.1
fmt.Println(x) // 7.1
}
|
反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。
只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。
一般使用反射修改结构体的字段,只要有结构体的指针,我们就可以修改它的字段。 结构体中只有可导出的字段是“可设置”的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import (
"fmt"
"reflect"
)
func main() {
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
fmt.Println("settability of v:", s.CanSet())
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
}
|
五、Go语言IsNil()和IsValid()——判断反射值的空和有效性
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示。
方 法 |
说 明 |
IsNil() bool |
返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil操作 |
IsValid() bool |
判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。 |
IsNil() 常被用于判断指针是否为空;IsValid() 常被用于判定返回值是否有效。
下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码。
反射值对象的零值和有效性判断:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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())
}
// 输出如下:
// var a *int: true
// nil: false
// (*int)(nil): false
// 不存在的结构体成员: false
// 不存在的结构体方法: false
// 不存在的键: false
|
六、Go语言通过反射修改变量的值
Go语言中类似 x、x.f[1] 和 *p 形式的表达式都可以表示变量,但是其它如 x + 1 和 f(2) 则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
对于 reflect.Values 也有类似的区别。有一些 reflect.Values 是可取地址的;其它一些则不可以。考虑以下的声明语句:
1
2
3
4
5
|
x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)
|
其中 a 对应的变量则不可取地址。因为 a 中的值仅仅是整数 2 的拷贝副本。
b 中的值也同样不可取地址。
c 中的值还是不可取地址,它只是一个指针 &x 的拷贝。
实际上,所有通过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可取地址的。
但是对于 d,它是 c 的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用 reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的 Value。
我们可以通过调用 reflect.Value 的 CanAddr 方法来判断其是否可以被取地址:
判定及获取元素的相关方法
使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表。
方 法 |
说 明 |
Elem() Value |
取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value |
Addr() Value |
对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机 |
CanAddr() bool |
表示值是否可寻址 |
CanSet() bool |
返回值能否被修改。要求值可寻址且是导出的字段 |
值修改相关方法
使用 reflect.Value 修改值的相关方法如下表所示。
Set(x Value) |
将值设置为传入的反射值对象的值 |
Setlnt(x int64) |
使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机 |
SetUint(x uint64) |
使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机 |
SetFloat(x float64) |
使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机 |
SetBool(x bool) |
使用 bool 设置值。当值的类型不是 bod 时会发生宕机 |
SetBytes(x []byte) |
设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机 |
SetString(x string) |
设置字符串值。当值的类型不是 string 时会发生宕机 |
以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。
**通过反射修改变量值的前提条件之一:**这个值必须可以被寻址。简单地说就是这个变量必须能被修改。
当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。
虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。
通过反射修改变量值可修改条件之一:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
取这个变量的地址或者这个变量所在的结构体已经是指针类型。
使用 reflect.ValueOf 进行值包装。
通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
使用 Value.Set 设置值。
七、Go语言通过类型信息创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
// 取变量a的反射类型对象
typeOfA := reflect.TypeOf(a)
// 根据反射类型对象创建类型实例
aIns := reflect.New(typeOfA)
// 输出Value的类型和种类
fmt.Println(aIns.Type(), aIns.Kind()) // *int ptr
}
|
八、Go语言通过反射调用函数
如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。
使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。