介绍
反射能力是一个程序自己检查自己的结构的能力,是一种元编程(metaprogramming)的形式。
类型和接口(Types and interfaces)
我们必须明确以下三个概念,因为反射和它们息息相关。
区分三个词:
- 静态类型(
static type
) - 基础类型(
underlying type
) - 接口类型(
interface types
)
静态类型和基础类型
- 可以说 Go 中的每一个类型都是静态类型。因为 Go 是静态类型的(Statically Typed)语言。即开发者自定义的类型也是一种静态类型。
- 基础类型是指 GO 内部定义的几种类型,如
int
、float64
等等。
如下方示例,
-
MyInt
是一个静态类型,j 的类型(就是指静态类型)就是MyInt
; -
int
也是一个静态类型,同时它也是一个基础类型; - 所以
i
和j
有不同的类型(静态类型),但有相同的基础类型; - 静态类型不同的变量相互之间是不可以直接赋值的。
type MyInt int
var i int
var j MyInt
静态类型和动态类型的区别(参考)
如果在编译时就知道某一个变量是什么类型,这个语言就是静态类型的编程语言。
所以开发者在开发时必须为每一个变量指定类型,它的好处就在于编译器可以做一些检查工作,即在编译阶段就发现了一些 bug。如果一种语言是动态的,那变量只在运行时有类型。
所以动态编程语言没有编译器检查它们在类型,这让开发变得快速。这类语言大多是脚本语言,即一般比较小,这样方便开发者自己去找 bug。
接口类型
接口类型是提供了一套固定的方法的类型。一个接口变量可以存储任何非接口的固定的值,只要这个值实现了接口的方法。
一个很好的例子就是 io.Reader
和 io.Writer
。下面这些使用都是可以的。
var r io.Reader // interface{}
r = os.Stdin // *io.File
r = bufio.NewReader(r) // *bufio.Reader
r = new(bytes.Buffer) // bytes.Buffer
// and so on
一定要注意: r
定义为 io.Reader
接口类型,不论 r
的值如何变化,是 os.Stdin
(*io.File
类型)还是 bufio.NerReader(r)
(*bufio.Reader
类型),它的静态类型都是 io.Reader
。
一个极重要的接口类型是空接口 interface{}
。 它表示了一组空的方法,因为任何类型都有零个或多个方法 ,所以值就可以是任何类型。(一些人说 Go 的 interface{}
是动态类型的,这是错误的。)
一个接口的表示
这里有一篇博文详细介绍了接口:Go Data Structures: Interfaces,可以看一下,这里简单总结一下。
一个接口类型的变量存了两部分:一个是具体的值,和这个值的类型描述符(type descriptor)。
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
上例中 r 包含了 (value, type)
,即 (tty, *os.File)
,*os.File
实现了 io.Reader
的接口。
所以我们可以用如下方法:
var w io.Writer
w = r.(io.Writer)
这个表达示叫 类型断言(type assertion),上例中断言了 r
的内部也实现了 io.Writer
,所以我们可以将它赋值给 w
,所以 w
包含一对信息 (tty, *os.File)
。
进一步地,我们还可以这样做:
var empty interface{}
empty = w
这样我们的空接口就包含了相同的一对信息 (tty, *os.File)
。这种方式很灵巧:一个空的接口可以包含任何值并且包含我们可能需要的所有信息。(这里不用断言,因为 it's known statically that w satisfies the empty interface)
有一点要注意,一个接口的内部的一对信息,必须是 (value, concrete type)
而不能是 (value, iinterface value)
。接口不能有接口类型的值。
下面我们来介绍反射。
反射
反射有三条法则:
- 从接口值反射为反射对象
- 从反射对象反射为接口值
- 更新值时必须是可更新的
从接口值反射为对象
reflect 包有两个类型: Type 和 Value,这两个类型让我们有方法 reflect.TypeOf
和 reflect.ValueOf
获取到接口内部的信息 reflect.Type
和 reflect.Value
。(同样通过 reflect.Value
也可以得到 reflect.Type
,我们稍后再说。)
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
当我们调用 reflect.TypeOf(x)
时,它会将 x
copy 一份到空的 interface{}
类型中,然后在 TypeOf()
从空接口中恢复到类型信息。
reflect.ValueOf()
方法同样从接口中恢复到值信息。
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
(由于 fmt
自动会获取到具体的类型的值,所以这里我们用了 String()
方法。)
-
reflect.Value
和reflect.Type
都有Kind()
方法是获取反射对象的 基础类型(underlying type
),而不是静态类型。 -
reflect.Value
上有Type()
方法,可以获取到值的类型 -
reflect.Value
上有Int()
等方法可以获取对应的值 -
reflect.Value
上有SetInt()
等方法,可以设置值(下面介绍)
从对象反射为接口值
给定一个 reflect.Value
我们可以恢复它对应接口的值,这用到 Interface()
方法。它会将 type
信息和 value
信息打包到 interface{}
中,并返回 interface{}
值。
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
要更新一个反射对象,它的值必须是可更新的
第三个法则很微妙,但也容易理解。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
上面的这个例子会 Panic:panic: reflect.Value.SetFloat using unaddressable value
。问题原因不是因为 7.1 是不可寻址的,可设置性也是反射的一个属性,可以通过 CanSet()
方法来获取。
为什么它是不可设置的呢?
因为上例中 reflect.ValueOf(x)
和其他普通方法 f(x)
一样,会 copy 一个 x 的值传入方法中,而不是 x 本身,所以如果我们需要通过反射修改一个变量的值时,必须传入它的地址 reflect.ValueOf(&x)
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type()) // *float64
fmt.Println("settability of p:", p.CanSet()) // false
但这样仍不够,因为 p
的类型是一个指针,所以我们需要设置它对应的值,所以需要方法 Elem()
。所以,变成下面:
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("settability of p:", p.Elem().CanSet()) // true
p.Elem().SetFloat(2.4) //
fmt.Println(x) // 2.4
这样就完成了通过反射的方式,更新一个变量的值。重申:传入一定要是地址,并且修改的是地址中对应的值(Elem()
方法获取值)。
结构体(Structs)
我们前面都是在以基础类型做示例,下面是一个结构体的例子。
t := T{23, "robin"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
fmt.Println(typeOfT) // main.T
for i := 0; i < s.NumField(); i++ {
filed := s.Field(i) // 返回类型 reflect.Value
fmt.Println(typeOfT.Field(i).Name) // A B
fmt.Println(filed.Type()) // int string
fmt.Println(filed.Interface()) // 23 robin
fmt.Println()
}
s.Field(0).SetInt(28) // 设置值
s.Field(1).SetString("cai") // 设置值
fmt.Println(t) // {28 cai}
基本是一样的,不过结构体中
-
s := reflect.ValueOf(&t).Elem()
获取元素的值 Value -
s.NumField()
获取值对应的长度 -
s.Field(i)
获取对应下标的元素的 Value
总结
记住这三条法则:
- 反射可以从接口值反射为反射对象
- 反射可以从反射对象反射为接口值
- 要更新反射对象,其值必须可设置
反射中还有许多在这里没讲到,比如收发 channel、分配内存、用 slices 和 maps、调方法等。可以阅读一下其他文章。
附:
- The Laws of Reflection
- Go Data Structures: Interfaces
- Effective Go