Golang Pearl 02

作为有运行时的语言,Go也实现了反射机制,这为我们提供了一种可以在运行时操作任意对象的能力。比如:

  1. 查看一个接口变量的具体类型
  2. 查看一个结构体中的字段
  3. 修改某个字段

反射

在Go语言中任何类型都由两部分组成,Value和Type两部分组成,在Go提供的reflect包中分别有两个函数可以测试这两个部分。

type User struct {
	id   int
	name string
}

func main() {
	m := User{1, "Tom"}
	fmt.Println(reflect.TypeOf(m))
	fmt.Println(reflect.ValueOf(m))
}

获取基础类型

在Go中的所有类型都离不开基础类型,最终都能回溯到某一个基础类型,因此可以使用对象的Value部分测试这个基础类型。

type User struct {
	id   int
	name string
}

func main() {
	m := User{1, "Tom"}
	// m := 1
	fmt.Println(reflect.ValueOf(m).Kind())
}

可以看到m的基础类型是struct。

遍历字段和方法

通过反射我们可以得知一个类型的字段和方法,这样我们就可以在运行时了解一个类的结构,这非常有用。

type User struct {
	id   int
	name string
}

func main() {
	m := User{1, "Tom"}
	// m := 1
	fmt.Println(reflect.ValueOf(m).Kind())
	for i := 0; i < reflect.ValueOf(m).NumField(); i++ {
		fmt.Println(reflect.ValueOf(m).Field(i))
	}
	for i := 0; i < reflect.ValueOf(m).NumMethod(); i++ {
		fmt.Println(reflect.ValueOf(m).Method(i))
	}
}

动态调用函数

反射机制的一个个非常重要的作用就是动态调用方法,使用这一方法能够实现很强大的功能。

func main() {
	u := User{"Jack", 20}
	v := reflect.ValueOf(u)
	mPrint := v.MethodByName("Print")
	args := []reflect.Value{reflect.ValueOf("prefix")}
	fmt.Println(mPrint.Call(args))
}

type User struct {
	Name string
	Age  int
}

func (u User) Print(prfix string) {
	fmt.Printf("%s: Name is %s, Age is %d", prfix, u.Name, u.Age)
}

MethodByName方法可以让我们根据一个方法名获取一个方法对象,然后我们构建好该方法需要的参数,最后调用Call就达到了动态调用方法的目的。

反射三原则

反射基于类型系统,我们在上面已经讨论了基本的反射应用,已经清楚了Go中value和type的体系。现在我们假定读者了解空接口和接口实现的条件,进行以下讨论。

反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”。

从用法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。通过一个简单的例子,我们可以知道怎样提取类型,怎样提取值。

func main() {
	var x float64 = 3.4
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())
	fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
	fmt.Println("value:", v.Float())
}

反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”。

Go语言中的反射也能创造自己反面类型的对象。通过interface方法将type和value打包,并填充到一个接口变量中,然后返回。

func main() {
	var x uint8 = 'x'
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())                            // uint8.
	fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
	x = uint8(v.Uint())                                       // v.Uint returns a uint64.

	k := v.Interface().(uint8)
	fmt.Println(k)
}

这里的uint8,就是原来的数值类型。因此,interface和valueof两个函数是正好相反的,只不过interface返回的是一个interface{}接口,值得注意的是,println函数接受的参数就是一个interface{}变量,然后在函数内部解包,使用对应的形式进行打印,因此我们可以这样做:

func main() {
	var x uint8 = 'x'
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())                            // uint8.
	fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
	x = uint8(v.Uint())                                       // v.Uint returns a uint64.

	k := v.Interface().(uint8)
	fmt.Println(k)

	fmt.Printf("value is %05d\n", v.Interface())
}

直接使用格式化输出打印v的值,但是在这里我们不能直接用v这个变量,因为它是一个reflect.value类型的变量。

反射第三定律:如果要修改“反射类型对象”,其值必须是“可写的”(settable)

现在我们能够解析出一个对象的属相,想要修改这个对象的属相,我们可能会想到这样做。

func main() {
	var x uint8 = 'x'
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())                            // uint8.
	fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
	x = uint8(v.Uint())                                       // v.Uint returns a uint64.

	k := v.Interface().(uint8)
	fmt.Println(k)

	fmt.Printf("value is %05d\n", v.Interface())

	v.SetInt(10)
}

但得到的结果却是一个panic:

panic: reflect: reflect.Value.SetInt using unaddressable value

这个问题的原因不在于提示的不可寻址,而在于v是不可写的。可写行是反射类型的一个属性,但不是所有的反射类型都拥有这一属性,可以使用CanSet()来验证可写行。

类型的可写行唯一标准就是这个变量是否拥有原值,我们知道v只是x的一个复制品,如果我们修改了v,x是不会变化的。这就让我们想到了使用指针的方式来修改x。但是光使用指针还不够,因为是使用指针我们得到的是指向x指针的一个复制品,这个复制品仍旧不可写,但我们想要写的不是指针,而是这个指针所指向的值。因此,我们使用Elem函数来还原指针指向的值,这个值就是原值,也就是可写的了。

func main() {
	var x uint8 = 'x'
	p := reflect.ValueOf(&x)
	fmt.Println("settability of p:", p.CanSet())

	v := p.Elem()
	fmt.Println("settability of v:", v.CanSet())
}

确定v可写之后,我们就能通过v来修改x了。

func main() {
	var x uint8 = 'x'
	p := reflect.ValueOf(&x)
	fmt.Println("settability of p:", p.CanSet())

	v := p.Elem()
	fmt.Println("settability of v:", v.CanSet())

	v.SetUint(99)
	fmt.Println(v.Interface())
	fmt.Println(x)
}

从结果我们也能看到,原值x也被修改了。只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。