计算中的反射是程序检查自身结构的能力,尤其是通过类型;这是元编程的一种形式。这也是一个很大的困惑来源。你可以先复习一下 Interface接口设计
类型和接口
我们先复习一下Go语言中的类型,因为反射机制是构建在类型系统之上的。
Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 int, float32, *MyType, []byte 等等。注意,这个类型是声明时候的类型,不是底层数据类型。
type MyInt int
var i int
var j MyInt
i的类型是int,j的类型是MyInt,但是它们有相同的底层类型int,如果不进行转换,它们就不能相互赋值。
类型的一个重要类别是接口类型,它表示固定的方法集。接口变量可以存储任何具体(非接口)值,只要该值实现接口的方法。一对著名的例子是io.Reader && io.Writer
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
任何实现了这些方法的类型被称为实现了 io.Reader或者io.Writer接口
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
无论r的值是多少,他的静态类型总是io.Reader
另一个重要的例子,空接口:
interface{}
它可以理解成任意类型
any
它表示方法的空集合,任何值都可以满足它,因为每个值都有零个或多个方法。
interface并不是动态类型,它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。
我们需要对接口有比较深刻的理解,因为反射和接口是密切相关的。
反射的定律
1.接口值到反射对象
反射只是一种检查接口变量中存储的类型和值对的机制,我们需要了解包反射中的两种类型:reflect.Type和reflect.Value 。这两种类型允许访问接口变量的内容,reflect 包中提供了两个函数 reflect.TypeOf && reflect.ValueOf 来获取接口变量类型和值。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
输出:
type: float64
您可能想知道接口在哪里,因为程序看起来是在传递 float64变量x,而不是接口值来反映。但是我们可以从reflect.TypeOf的函数签名来看,它包含一个空接口:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
当我们调用reflect.TypeOf,首先会将x转换成interface空接口类型。之后解压该接口以恢复类型信息。
同理,我们可以尝试使用reflect.ValueOf
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
输出:
value: <float64 Value>
reflect.Value有一个返回反射reflect.Type方法。Type和Value都有一个Kind方法,该方法返回一个常量,表示存储的是哪种底层类型:Uint、Float64、Slice等等。还有一些名为Int和Float的方法可以获取存储在其中的值:
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())
输出:
type: float64
kind is float64: true
value: 3.4
2.反射对象到接口值
和物理反射一样,Go中的反射也会产生自己的逆反射。我们可以使用接口方法 Interface 恢复接口值;实际上,该方法将类型和值信息打包回接口表示,并返回结果:
// Interface returns v's current value as an interface{}.
// It is equivalent to:
// var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}
我们可以这么做:
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
打印由反射对象v表示的float64值: 反射从接口值到反射对象,然后再返回。
3.要修改反射对象,该值必须是可设置的。
第三定律是最微妙、最令人困惑的,但如果我们从第一原理出发,就很容易理解。
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1) // Error: will panic.
输出:
settability of v: false
panic: reflect.Value.SetFloat using unaddressable value
可设置性有点像可寻址性,但更严格。反射对象可以修改用于创建反射对象的实际存储。可设置性由反射对象是否保留原始项决定:
var x float64 = 3.4
v := reflect.ValueOf(x)
我们应该理解在Go语言中,参数的传递是进行了值拷贝。因此我们无法对原x进行重新设置。在代码层面看来,x 其实是从reflect.ValueOf函数里的代码创建的,所以使用panic来杜绝可能引起的混乱。
如果我们期望改变数据本身,我们必须向函数传递x的指针(指向x的指针)
var x float64 = 3.4
p := reflect.ValueOf(&x)
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
//
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1)
fmt.Println(x)
输出:
type of p: *float64
settability of p: false
settability of v: true
7.1
反射对象p是不可设置的,它本质上是*p,是一个地址值。我们真正需要操作的是p指向的内存空间。所以这里我们用p.Elem来获取。反射值需要某个对象的地址,以便修改它们所代表的内容。
结构体修改
只要我们有这个结构的地址,我们就可以修改它字段的值。
下面是一个分析结构值t的简单示例,我们使用结构的地址创建反射对象,因为我们希望通过反射对象提取字段的名称和值,且稍后对其进行修改。
type T struct {
Name string `json:"name"`
age int `json:"age"`
}
t := T{"skidoo", 23}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
if f.CanInterface() {
fmt.Printf("%d: %s %s = %v\n", i,typeOfT.Field(i).Name, f.Type(), f.Interface())
}
}
输出:
0: A int = 23
我们可以看到只有大写的字段才能从反射对象中获取值,同理我们再设置值得时候也如此。
s.Field(0).SetString("Sunset Strip")
// s.Field(1).SetInt(77) //panic
fmt.Println("t is now", t)
输出:
t is now {23 Sunset Strip}
如果我们修改程序,使s是从t而不是&t创建的,那么对SetString的调用将失败,因为t的字段将不可设置。
结论
一旦你理解了这些规律,Go中的反射就会变得更容易使用,尽管它仍然很微妙。这是一个强大的工具,应该小心使用,除非绝对必要,否则应避免使用。
还有很多关于反射的内容我们还没有涉及到——在通道上发送和接收、分配内存、使用切片和映射、调用方法和函数——但这篇文章已经足够长了。我们将在后面的文章中介绍其中一些主题。