Go中的反射

Go 语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制称为反射。

reflect.Type 和 reflect.Value

反射功能由reflect 包提供,它定义了两个重要的类型:Type 和 Value。Type 表示Go 语言的一个类型,它是一个有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分,比如一个结构的各个字段或者一个函数的各个参数。reflect.Type 接口只有一个实现,即类型描述符,接口值中的动态类型也是类型描述符。

	t := reflect.TypeOf(3)
	fmt.Println(t.String())  // int
	fmt.Println(t)           // int

上面的 TypeOf(3) 调用把数值3赋给 interface{} 参数。把一个具体值赋给一个接口类型时会发生一个隐式类型转换,转换会生成一个包含两部分内容的接口值:动态类型部分是操作数的类型(int),动态值部分是操作数的值(3)。
因为 reflect.TypeOf 返回一个接口值对应的动态类型,所以它返回总是具体类型(而不是接口类型)。
reflect.Value 和 interface{} 都可以包含任意的值。二者的区别是空接口(interface{})隐藏了值的布局信息、内置操作和相关方法。所以除非我们知道它的动态类型,并用一个类型断言来渗透进去,否则我们对所包含值能做的事情很少。作为对比,Value 有很多方法可以用来分析所包含的值,而不用知道它的类型。reflect.Value 的 Kind 方法可以区分不同的类型。

func Any(value interface{}) string {
	return formatAtom(reflect.ValueOf(value))
}

func formatAtom(v reflect.Value) string {
	switch v.Kind() {
	case reflect.Invalid:
		return "invalid"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return strconv.FormatInt(v.Int(), 10)
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,reflect.Uintptr:
		return strconv.FormatUint(v.Uint(), 10)
	case reflect.Bool:
		return strconv.FormatBool(v.Bool())
	case reflect.String:
		return strconv.Quote(v.String())
	case reflect.Chan, reflect.Func, reflect.Ptr,reflect.Slice, reflect.Map:
		return v.Type().String() + " 0x" + strconv.FormatUint(uint64(v.Pointer()), 16)
	default:
		return v.Type().String() + " value"
	
	}
}

使用 reflect.Value 来设置值

变量是一个可寻址的存储区域,其中包含了一个值,并且它的值可以通过地址来更新。

	x := 2                       
	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)

事实上,通过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可寻址的。但 d 是通过对 c 中的指针提领得来的,所以它是可寻址的。可以通过这个方法,调用reflect.ValueOf(&x).Elem() 来获取任意变量x 可寻址的 Value 值。
我们可以通过变量的 CanAddr 方法来询问 reflect.Value 变量是否可寻址:

	fmt.Println(a.CanAddr())	// false
	fmt.Println(b.CanAddr())	// false
	fmt.Println(c.CanAddr())	// false
	fmt.Println(d.CanAddr())	// true

我们可以通过一个指针来间接获取一个可寻址的 reflect.Value,即使这个指针是不可寻址的。可寻址的常见规则都在反射包里有对应项。比如,slice 的脚标表达式 e[i]隐式地做了指针去引用,所以即使 e 是不可寻址的,这个表达式仍然是可寻址的。类似地,reflect.ValueOf(e).Index(i) 代表一个变量,尽管 reflect.ValueOf(e) 是不可寻址的,这个变量也是可寻址的。
从一个可寻址的 reflect.Value() 获取变量需要三步。首先,调用 Addr(),返回一个 Value,其中包含一个指向变量的指针,接下来,在这个 Value 上调用 Interface(),会返回一个包含这个指针的 interface{} 值。最后,如果我们知道变量的类型,我们可以使用类型断言来把接口转换为一个普通指针。之后就可以通过这个指针来更新变量了:

	x := 2
	d := reflect.ValueOf(&x).Elem()   // d 代表变量 x
	px := d.Addr().Interface().(*int)  // px := &x
	*px = 3							  // x = 3
	fmt.Println(x)					  // 3

还可以指针通过可寻址的 reflect.Value 来更新变量,不过通过指针,而是直接调用 reflect.Value.Set 方法:

	d.Set(reflect.ValueOf(4))
	fmt.Println(x)					// 4

上面的变量和值都是 int 类型,但如果变量类型是 int64,这个程序就会崩溃。所以确保这个值对于变量类型是可赋值的是很重要的一件事:

	d.Set(reflect.ValueOf(int64(5)))    // 崩溃: int64 不可赋值给 int

当然,在一个不可寻址的 reflect.Value 上调用 Set 方法也会崩溃。但需要注意的是,在指向 interface{} 变量的 reflect.Value 上调用 SetInt 会崩溃(尽管使用 Set 就没有问题)。

	x := 2
	b := reflect.ValueOf(x)
	b.Set(reflect.ValueOf(4))  // 崩溃: 在不可寻址的值上使用 Set
	x := 1
	rx := reflect.ValueOf(&x).Elem()
	rx.SetInt(2)						// OK, x = 2
	rx.Set(reflect.ValueOf(3))		// OK, x = 3
	rx.SetString("hello")			// 崩溃,字符串不能赋给整数
	rx.Set(reflect.ValueOf("hello")) // 崩溃,字符串不能赋给整数

	var y interface{}
	ry := reflect.ValueOf(&y).Elem()
	ry.SetInt(2)							// 崩溃:在指向接口的 Value 上调用 SetInt
	ry.Set(reflect.ValueOf(3))           // OK, y = int(s)
	ry.SetString("hello")				// 崩溃: 在指向接口的 Value 上调用 SetString
	ry.Set(reflect.ValueOf("hello"))		// OK, y = "hello"

一个可寻址的 reflect.Value 会记录它是否通过遍历一个未导出字段来获得的,如果是这样,则不允许修改。所以在更新变量前用 CanAddr 来检查并不能保证正确。CanSet 方法才能正确地报告一个 reflect.Value 是否可寻址且可更改:

	stdout := reflect.ValueOf(os.Stdout).Elem()
	fmt.Println(stdout.Type())
	fd := stdout.FieldByName("fd")
	fmt.Println(fd.CanAddr(), fd.CanSet())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值