谈谈Go语言的反射三定律

2020-01-28 12:15:17丽君

下面这段代码不能正常工作,但是非常值得研究:


var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你运行这段代码,它会抛出抛出一个奇怪的异常:


panic: reflect.Value.SetFloat using unaddressable value

这里问题不在于值 7.1 不能被寻址,而是因为变量 v 是“不可写的”。“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。

我们可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”。对于上面的例子,可以这样写:


var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

上面这段代码打印结果是:


settability of v: false

对于一个不具有“可写性”的 Value类型变量,调用 Set 方法会报出错误。首先,我们要弄清楚什么“可写性”。

“可写性”有些类似于寻址能力,但是更严格。它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个事实决定的:反射对象是否存储了原始值。举个代码例子:


var x float64 = 3.4
v := reflect.ValueOf(x)

这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝,而非 x 本身。想象一下,如果下面这行代码能够成功执行:


v.SetFloat(7.1)

答案是:如果这行代码能够成功执行,它不会更新 x ,虽然看起来变量 v 是根据 x 创建的。相反,它会更新 x 存在于 反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。

这看起来很诡异,事实上并非如此,而且类似的情况很常见。考虑下面这行代码:


f(x)

上面的代码中,我们把变量 x 的一个拷贝传递给函数,因此不期望它会改变 x 的值。如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f,如下:


f(&x)

你应该很熟悉这行代码,反射的工作机制是一样的。如果你想通过反射修改变量 x,就咬吧想要修改的变量的指针传递给 反射库。

首先,像通常一样初始化变量 x,然后创建一个指向它的 反射对象,名字为 p:


var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

这段代码的输出是: