go reflect
反射核心
反射的核心是两个对象,分别是 reflect.Type 和 reflect.Value。 它们分别代表了 go 语言中的类型和值。我们可以通过 reflect.TypeOf 和 reflect.ValueOf 来获取到一个变量的类型和值。
func main() {
var a = 1
fmt.Println(reflect.ValueOf(a))
fmt.Println(reflect.TypeOf(a))
}
反射定律
三条反射定律:
- 反射可以将 interface 类型变量转换成反射对象。
- 反射可以将反射对象还原成 interface 对象。
- 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
反射可以将 interface 类型变量转换成反射对象
其实也就是上面的 reflect.Type 和 reflect.Value,我们可以通过 reflect.TypeOf 和 reflect.ValueOf 来获取到一个变量的反射类型和反射值。
反射可以将反射对象还原成 interface 对象
我们可以通过 reflect.Value.Interface 来获取到反射对象的 interface 对象,也就是传递给 reflect.ValueOf 的那个变量本身。 不过返回值类型是 interface{},所以我们需要进行类型断言。
func main() {
var a = 1
i := reflect.ValueOf(a).Interface()
fmt.Println(i.(int))
}
如果要修改反射对象,那么反射对象必须是可设置的(CanSet)
我们可以通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过 reflect.Value.Set 来修改反射对象的值。
那什么情况下一个反射对象是可设置的呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。在我们传递一个值给 reflect.ValueOf 的时候,reflect.ValueOf 实际上会返回该值的一个副本(包括指针),当我们对副本进行修改的时候,实际上对原数据没有任何影响。所以 go 语言在这里做了一个限制,如果我们传递进 reflect.ValueOf 的变量是一个普通的变量,那么在我们设置反射对象的值的时候,就会报错。
func main() {
var a = 1
p := &a
v := reflect.ValueOf(p).Elem()
fmt.Println(v) // 1
fmt.Println(*p) // 1
var b = 2
p = &b
fmt.Println(v) // 1
fmt.Println(*p) // 2
}
并且,当传入的值是一个指针时,i2.CanSet() 的返回值依旧是 false,这是因为如果我们直接修改指针的值是没有任何意义的,因为我们如果修改了指针变量,也就相当于将该指针指向了另外的地址,这也是没有任何意义的。所以,我们需要调用 Elem 方法,获取指针实际指向的值。
func main() {
var a = 1
i1 := reflect.ValueOf(a)
fmt.Println(i1.CanSet()) // false
i2 := reflect.ValueOf(&a)
fmt.Println(i2.CanSet()) // false
fmt.Println(i2.Elem().CanSet()) // true
}
Elem 方法
reflect.value 的 Elem 方法
reflect.Value 的 Elem 方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 Elem 方法的反射对象,必须是一个指针或者一个接口()。
在使用其他类型的 reflect.Value 来调用 Elem 方法的时候,会 panic
type MyStruct struct {
name string
}
func main() {
ms := MyStruct{name: "test"}
v1 := reflect.ValueOf(&ms)
v2 := reflect.ValueOf(ms)
fmt.Println(v1.Elem().CanSet()) // true
fmt.Println(v2.Elem().CanSet()) // panic: reflect: call of reflect.Value.Elem on struct Value
}
对于指针,则作用类似于解指针。对于接口,因为接口中包含了类型和数据,所以 Elem 则是返回接口的数据部分。
reflect.Type 的 Elem 方法
reflect.Type 的 Elem 方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于 reflect.Type 来说,能调用 Elem 方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的 reflect.Type 调用 Elem 方法会 panic。需要注意的是,如果我们要获取 map 类型 key 的类型信息,需要使用 Key 方法,而不是 Elem 方法。
func main() {
var a = []int{1, 3, 4}
m := make(map[int]string)
fmt.Println(reflect.TypeOf(a).Elem()) // int
fmt.Println(reflect.TypeOf(m).Elem()) // map直接使用 Elem 返回的是值的类型
fmt.Println(reflect.TypeOf(m).Key()) // 获取键的类型需要使用 key 方法
}
interface 方法
该函数接收一个 Value 类型的参数 v,并返回一个 interface{} 类型的值。该函数的作用是将 v 转换为一个 interface{} 类型的值。需要注意的是,返回值只包括值部分,并不包含类型部分,使用想要使用还需要进行类型断言,转换为具体的值。
func main() {
var i = 1
var a interface{} = &i
b := reflect.ValueOf(a).Interface()
fmt.Println(b == a) // true
d := reflect.ValueOf(i).Interface() // d == (interface{} = 1)
}
Kind
在 go 中,我们可以自定义各种各样的类型。
type MyType1 int
type MyType2 float32
但是在底层,我们定义的类型都是 go 的基本类型之一。这个基本类型就是 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
)
所以,我们可以通过有限的 reflect.Type 的 Kind 来进行类型判断。枚举出可能出现的类型。
func main() {
var a = 1
switch reflect.ValueOf(a).Kind() {
case reflect.Int:
fmt.Println("yes")
}
}
addressable
在 go 的反射系统中有两个关于寻址的方法:CanAddr 和 CanSet。
CanAddr 方法的作用是判断反射对象是否可以寻址,也就是说,如果 CanAddr 返回 true,那么我们就可以通过 Addr 方法来获取反射对象的地址。如果 CanAddr 返回 false,那么我们就不能通过 Addr 方法来获取反射对象的地址。对于这种情况,我们就无法通过反射对象来修改变量的值。
但是,CanAddr 是 true 并不是说 reflect.Value 一定就能修改变量的值了。reflect.Value 还有一个方法 CanSet,只有 CanSet 返回 true,我们才能通过反射对象来修改变量的值。
func main() {
var a = 1
fmt.Println(reflect.ValueOf(&a).Elem().CanAddr()) // true
fmt.Println(reflect.ValueOf(&a).CanAddr()) // false
fmt.Println(reflect.ValueOf(a).CanAddr()) // false
}
总结
- reflect 包提供了反射机制,可以在运行时获取变量的类型信息、值信息、方法信息等等。
- go 中的 interface{} 实际上包含了两个指针,一个指向类型信息,一个指向值信息。正因如此,我们可以在运行时通过 interface{} 来获取变量的类型信息、值信息。
- reflect.Type 代表一个类型,reflect.Value 代表一个值。通过 reflect.Type 可以获取类型信息,通过 reflect.Value 可以获取值信息。
- 反射三定律:
- 反射可以将 interface 类型变量转换成反射对象。
- 反射可以将反射对象还原成 interface 对象。
- 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
- reflect.Value 和 reflect.Type 里面都有 Elem 方法,但是它们的作用不一样:
- reflect.Type 的 Elem 方法返回的是元素类型,只适用于 array、chan、map、pointer 和 slice 类型的 reflect.Type。
- reflect.Value 的 Elem 方法返回的是值,只适用于接口或指针类型的 reflect.Value。
- 通过 reflect.Value 的 Interface 方法可以获取到反射对象的原始变量,但是是 interface{} 类型的。
- Type 和 Kind 都表示类型,但是 Type 是类型的反射对象,Kind 是 go 类型系统中最基本的一些类型,比如 int、string、struct 等等。
- 如果我们想通过 reflect.Value 来修改变量的值,那么 reflect.Value 必须是可设置的(CanSet)。同时如果想要 CanSet 为 true,那么我们的变量必须是可寻址的。
- 我们有很多方法可以创建 reflect.Type 和 reflect.Value,我们需要根据具体的场景来选择合适的方法。
- reflect.Type 和 reflect.Value 里面,都有一部分方法是通用的,也有一部分只适用于特定的类型。如果我们想要调用那些适用于特定类型的方法,那么我们必须先判断 reflect.Type 或 reflect.Value 的类型(这里说的是 Kind),然后再调用。