go reflect

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),然后再调用。

参考文章:深入理解 go reflect - 反射基本原理

posted @   InheritZe  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示