Golang Pearl 02
作为有运行时的语言,Go也实现了反射机制,这为我们提供了一种可以在运行时操作任意对象的能力。比如:
- 查看一个接口变量的具体类型
- 查看一个结构体中的字段
- 修改某个字段
反射
在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也被修改了。只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。