文章目錄
  1. 1. 前言
  2. 2. 类型与接口
  3. 3. 接口的表示法
  4. 4. 第一反射定律
    1. 4.1. 从接口值 到 反射对象 的反射
  5. 5. 反射第二定律
    1. 5.1. 从反射对象到接口值
  6. 6. 第三反射定律
    1. 6.1. 如要更改一个反射对象,其值须为settable
  7. 7. 结构
  8. 8. 总结

前言

反射指程序在运行时检查自身结构(尤其 类型)的能力,是元程序设计的一种形式,同时也是很多人为这迷惑的地方。
本文针对Go来说明反射如何工作,不同语言反射模型不同,有些语言早根本不支持反射,所以本文内容仅适用Go.

类型与接口

因为反射建立在类型系统之上,首先回顾一下Go的类型。

Go是静态类型。 每个变量都有静态类型,意味着,编译时*即有唯一&确定的类型。

1
2
3
4
type MyInt int

var i int
var j MyInt

i与j有着不同的静态类型,它们虽然有着相同uderlying type,但未经强制转换不是不能直接相互赋值的。

接口代表指定的方法集合,是一种重要的类型。接口变量可以存储任何实现了接口所定义方法的类型的变量。比如说io.Reader io.Writer。

1
2
3
4
5
6
7
8
9
// 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)
}

任何实现了Read方法的类型都可以赋给io.reader。

1
2
3
4
5
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

需要明白的是,不管r被赋予何值,其类型永远是io.Reader。因为,Go是静态类型的,而r的静态类型是io.Reader.

一个极其重要的接口类型例子是空接口。

1
interface{}

它代表空方法集合,既然任何类型都有至少0个方法,那么自然都实现了interface{}接口。

有些人认为Go的接口是动态类型的,这是误解。接口也是静态类型的,一个接口变量永远有着固定的静态类型,虽然在运行其存储的值可以被改变类型,但其值一定是满足接口的。

接口的表示法

关于接口类型的表示法有一篇详细的文章, 这里只概括性地说明。

接口变量总是存储着两个信息: 被赋予的具体+值的类型描述。更准确地说是,是实现了该接口的具体类型的数据项,而类型描述是该具体类型的完整信息。例如

1
2
3
4
5
6
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty

r是保存着 (tty, *os.File)信息的,虽然通过r只能调用Read()方法,但是它依然保存着tty的完整信息,这也是为什么我们能够执行这样的操作。

1
2
var w io.Writer
w = r.(io.Writer)

通过类型断言r中的具体类型tty也实现了Write()方法,我们可以将其赋值给w, w也保存着 (tty, *os.File)信息对。

接口的静态类型决定着通过它可以调用哪些方法,而其存储的实际数据项则可能包含着更大的方法集。

接着,我们也可以这样赋值。

1
2
var empty interface{}
empty = w

它也有着同样的信息对:这很方便,我们可以给它赋任何值,而且它也会保留该值的完整信息。

(这里将w赋值给empty没有作类型强制转换,而之前将r赋值给w之前则作了断言,因为w并不是r的子集,而interface{}则是能静态确定满足条件的)

总之需要明白的是接口存储的是 (value, concrete type)信息,而不是 (value, interface type),接口不保存接口类型。

(注:接口本身具自己的静态类型,而它存储的则是被赋予的确定类型的值的信息,对于非接口变量,本身的静态类型==被赋予值的类型,所以并不需要再存储额外的类型信息)

现在我们可以开始谈反射了。

第一反射定律

从接口值 到 反射对象 的反射

最基本的反射是检查接口存储的(值,类型)信息对。 起步之前我们需要了解reflect包中的
1,两个类型TypeValue. 通过它们可以接触到接口变量的内容。
2,两个方法reflect.TypeOf and reflect.ValueOf, 从接口变量是提取上述两种信息。

(事实上通过reflect.Value可以很方便地获得 reflect.Type信息,不过我们先将其分开)

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
fmt.Println("value:", reflect.ValueOf(x))
fmt.Println("value:", reflect.ValueOf(x).Float())

看起来没有用到接口,不过

1
func TypeOf(i interface{}) Type

输出是

1
2
3
type: float64
value: <float64 Value>
value: 3.4

(接下来可能会省略包名)
reflect.Type 和 reflect.Value都有大量的方法可供操作,比如
1 Value可以通过Type()获得Type
2 都有Kind()方法,返回一个常量,表明存储何种类型的数据项。
3 Value有如Int,Float的方法,可以抓取其中存储的值。(见上例)
(也有SetInt SetFloat方法,不过需要在理解settability之后讨论)

关于kind(注:省略了许多类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Kind uint

const (
Invalid Kind = iota
Bool
Int
Uint
……
Uintptr
Float64
Complex64
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)

reflect包中有一此值得单独说明的属性。
1 为使api简洁,Value的set,get方法全部以该类别的最大类型来操作:int64 , float64,所以有时需要自行转换至实际的类型。

1
2
3
var x uint8 = 'x'
v := reflect.ValueOf(x) // return int64
x = uint8(v.Uint())

2 Kind返回的是底层类型而不是静态类型。也就是说自定义类型type MyInt int的变量会被Kind识别为int类型,它不能区别int与myint,但Type可以。

反射第二定律

从反射对象到接口值

就像物理的反射,Go的反射也有它的可逆性。

从一个reflect.Value我们可以通过interface方法来恢复出一个接口值。事实上这个方法将
(value, concrete type)对重新打包成一个接口的表述并将其返回。

1
func (v Value) Interface() interface{}

因此我们可以

1
2
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

来打印反射变量v所表述的float64值。

事实上,因为println, printf等函数接受的参数都是interface{}类型,我们可以直接

1
2
fmt.Println(v.Interface())
fmt.Printf("value is %7.1e\n", v.Interface())

无需先做断言,print类函数将自己恢复底层类型信息。

简而言之,Interface()方法就ValueOf()方法的逆过程,除了它的返回类型总是interface{}之类。

第三反射定律

如要更改一个反射对象,其值须为settable

这也是三条定律中最奥妙最有迷惑性的一条,但如果我们重新从第一定律开始,还是足够好懂的。

以下代码不能真正运行,但值得一研究。

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

如果运行就会报如下错误:

1
panic: reflect.Value.SetFloat using unaddressable value

当然实际问题不是7.1的值不可寻址,而是变量v不可设置。可设置性是Value的类型的属性,而且不是每种反射Value都具有。

使用CanSet()方法可以确认Value的可设置性。

1
2
3
4
5
6
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
###output###
settability of v: false
###output###

那么到底什么是可设置性?

类似可寻址,都是以一个bit来表示,但可设置性更加严格。它表示反射对象可以真正地修改用于创建这个对象的原始内存内容。它取决于反射对象是否拥有原始内容

当我们进行

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

是值传递,即传递的是x的copy。所以Set操作即使被允许,也不会改变原来的x,所以为了避免使用者混淆,将其归类为非法操作。

所以从根本上来说,这和普通函数的值传递与地址传递并无根本区别,只是将其设置为了非法操作。

所以如果我们想通过反射更改原始变量,我们应该进行地址传递。

1
2
3
4
5
6
7
8
9
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())

###output###
type of p: *float64
settability of p: false
###output###

想要获得*p指向的值,我们可以调用Value的 Elem()方法。

测试Elem()返回值的可设置性

1
2
3
4
5
6
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

###output###
settability of v: true
###output###

现在我们可以通过v来改变x的值了

1
2
3
4
5
6
7
8
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

###output###
7.1
7.1
###output###

反射可能不太好懂,但它依然遵循语言的规则,虽然中间多了Value和Type的伪装。

结构

在我们之前的例子中,v不是指针而是指针的衍生物。因此通常可以用这种方式来改变结构体 的值。既然有了结构体的地址,那么就能改变它的Field值。

以下例子中,我们通过结构体的地址来创建反射变量,并使用reflect包中Type提供的方法来迭代遍历所有Field。请注意我们是通过reflect对象来提取结构体中的Filed名,提取出来的类型也是reflect.Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}

###output###
0: A int = 23
1: B string = skidoo
###output###

值得说明的是:只有大写开头的Field名才是可设置的(settable).

1
2
3
4
5
6
7
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

###output###

t is now {77 Sunset Strip}
###output###

如果不是从&t而是t来反射,那么set操作就会报错。

总结

如果理解了以上3条定律,那么反射就会好懂很多。当然反射的使用依然需要细心与谨慎,而且请在必要时使用。

还有许多没有提及的内容: 在channel中传递, 内存分配, slice与map, 方法与函数调用, 篇幅所限只能再在其它文章中说明。

文章目錄
  1. 1. 前言
  2. 2. 类型与接口
  3. 3. 接口的表示法
  4. 4. 第一反射定律
    1. 4.1. 从接口值 到 反射对象 的反射
  5. 5. 反射第二定律
    1. 5.1. 从反射对象到接口值
  6. 6. 第三反射定律
    1. 6.1. 如要更改一个反射对象,其值须为settable
  7. 7. 结构
  8. 8. 总结