什么是反射

反射是 go 语言提供的一种在运行时动态获取变量类型和值的手段。

有时候我们可能需要编写一个函数,来处理当时尚不清楚类型的参数,可能是该类型当时尚不存在,也可能是其没有明确的表达式。

比如说,我要编写一个函数,接收一个 any 类型的参数,打印出该参数的实际类型以及其方法列表。该需求通过正常方案是没办法实现的,必须使用反射,来得到其类型和其方法列表。

反射的使用

所有反射的功能,均由 reflect 包提供。该包中包含了反射功能最核心的两个类型,一是 reflect.Type ,该类型用来描述一个变量的具体类型;二是 reflect.Value ,该类型用来描述一个变量的具体的值。

reflect.TypeOf

函数 reflect.TypeOf() 接收任意的类型,并以 reflect.Type 返回其具体类型。

1
2
3
t := reflect.TypeOf(3)  // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"

这里额外提一嘴,将一个具体类型的值,作为接口类型传递时,在内部其实做了一个隐式的接口转换,它会创建一个包含两个信息的接口值,一个表示其具体类型(此处是 int),另一个表示其具体的值(此处是3)。

所以当你通过 TypeOf 传递一个接口类型的具体实现时,得到的将会是其具体类型,而非接口类型。说起来比较晦涩,可以通过以下例子理解一下:

1
2
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"

reflect.ValueOf

函数 reflect.ValueOf() 同样接受一个 any 类型的参数,并返回一个装载其值的 reflect.Value 类型。该类型可以用来装载任意类型的值。

1
2
v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v) // "3"

reflect.ValueOf 的逆操作是 reflect.Value.Interface() 方法。它返回一个 any 类型,装载着与 reflect.Value 相同的具体值。

1
2
v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface() // an interface{}

any 类型和 reflect.Value 类型都可以用来表示任何值,但不一样的是 any 类型隐藏了其内部的所有实现,我们只有知道其具体类型并将其转换为具体类型,才能访问其内部数据乃至其方法。而 Value 类型则提供了很多方法来访问其内部数据,无论它的具体类型是什么。

Kind

在 go 中,由于数组、切片乃至 map 的存在,其数据类型可以说是无穷无尽的。因为 [1]int[2]int 表示的是两种类型,同样的 map[int]stringmap[int64]string 表示的也是不同的类型。这时候我们就需要知道这种类型的种类了,在 go 中,我们将称其为 Kind

reflect 包中定义了一个 Kind 类型,并罗列了所有种类对应的 Kind 值,如下所示:

1
type Kind uint
Kind Value Kind Value Kind Value
Invalid 0 Bool 1 Int 2
Int8 3 Int16 4 Int32 5
Int64 6 Uint 7 Uint8 8
Uint16 9 Uint32 10 Uint64 11
Uintptr 12 Float32 13 Float64 14
Complex64 15 Complex128 16 Array 17
Chan 18 Func 19 Interface 20
Map 21 Pointer 22 Slice 23
String 24 Struct 25 UnsafePointer 26

可以通过 reflect.Type.Kind() 来获取一个类型的种类。

有了 Kind 的加持,我们基本上就可以实现解析所有类型的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func parseType(v any) {
kind := reflect.TypeOf(v).Kind()
switch kind {
case reflect.Int, reflect.Int64, reflect.Int16, reflect.Int32, reflect.Int8:
fmt.Printf("%s : %d \n", reflect.TypeOf(v).String(), reflect.ValueOf(v).Int())
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8:
fmt.Printf("%s : %d \n", reflect.TypeOf(v).String(), reflect.ValueOf(v).Uint())
case reflect.Bool:
fmt.Printf("%s : %b \n", reflect.TypeOf(v).String(), reflect.ValueOf(v).Bool())
case reflect.Float64, reflect.Float32:
fmt.Printf("%s : %f \n", reflect.TypeOf(v).String(), reflect.ValueOf(v).Float())
case reflect.String:
fmt.Printf("%s : %s \n", reflect.TypeOf(v).String(), reflect.ValueOf(v).String())
case reflect.Array, reflect.Slice:
fmt.Printf("%s : [ \n", reflect.TypeOf(v).String())
for i := 0; i < reflect.ValueOf(v).Len(); i++ {
parseType(reflect.ValueOf(v).Index(i).Interface())
}
fmt.Println("]")
// ...篇幅限制,更多类型自行拓展
}
}
func main() {
a := [...]int{1, 2, 3, 4, 5}
parseType(a)
}

运行结果:

1
2
3
4
5
6
7
[5]int : [ 
int : 1
int : 2
int : 3
int : 4
int : 5
]

操控 reflect.Value

虽然 reflect.Value 类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index 方法只能对 Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致 panic 异常。

以下总结一下各种类类型能够使用的方法:

Kind Methods
Array Len,Cap,Index,Slice,Slice3
Slice Len,Cap,Index,Slice,Slice3,Append,SetLen,SetCap
Chan Cap,Len,Close,Recv,RecvDir,Send,SendDir,TryRecv,TrySend
Func Call,CallSlice,IsNil
Interface Elem,IsNil
Map Len,IsNil,Keys,MapIndex,MapKeys,MapRange,MapIter,SetMapIndex,Delete
Ptr Elem,IsNil,Pointer,Set
Struct Field,FieldByIndex,FieldByName,FieldByNameFunc,NumField
UnsafePointer Pointer
String Len,String
Bool Bool
IntInt8Int16Int64 Int
Int32 Int,Rune
Uint8 Uint,Bytes
UintUint16Uint32Uint64Uintptr Uint
Float32Float64 Float
Complex64Complex128 Complex

在反射的使用场景中,很大部分是需要对参数的值进行修改的,譬如说 encoding/json 包中的 Unmarshal 函数,会将 json 数据解析到一个结构体参数中,此处传递的参数必须要是一个结构体指针。

此时,就需要涉及到 reflect.Value 的另外一个属性了,即可修改性。要对一个变量进行修改,首先要满足两个条件:

  1. 该变量是可到达的
  2. 该变量是可修改的(非导出字段是禁止修改的)

回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:

1
2
3
4
5
x := 2                   // value   type    variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。

reflect.Value 提供了两个方法来判断其是否满足上面的两个条件,一个是 value.CanAddr() ,用来判断该 value 是否可以被取地址;另一个是 value.CanSet() ,用来判断该 value 的值是否能被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := struct {
field1 string
Field2 int
}{}

a := reflect.ValueOf(s)
fmt.Println(a.CanAddr(), a.CanSet()) // false false

bv := reflect.ValueOf(&s).Elem()
bt := reflect.TypeOf(&s).Elem()
fmt.Println(bv.CanAddr(), bv.CanSet()) // true true

for i := 0; i < bv.NumField(); i++ {
cv := bv.Field(i)
ct := bt.Field(i)
fmt.Println(ct.Name, cv.CanAddr(), cv.CanSet())
}
}
1
2
3
4
false false      
true true
field1 true false
Field2 true true

在确定 reflect.Value 的值是可修改的后,就可以调用 reflect.Value 的一系列 Set 方法来修改变量的值了。

1
2
3
4
5
6
7
8
9
value.Set()
value.SetString()
value.SetInt()
value.SetBool()
value.SetBytes()
value.SetComplex()
value.SetFloat()
value.SetUint()
value.SetPointer()

使用这一系列的 Set 方法时需要注意,以下两种情况会导致程序 Panic:

  1. value 为不可到达或不可修改的值
  2. 设置的类型与 value 的实际类型不符

若是不想使用 Set 系列方法,也能通过取地址、断言并赋值等手段来修改值,不过这种方式比较繁琐,不是很推荐。

1
2
3
4
5
v := 10
vv := reflect.ValueOf(&v).Elem()
vvp := vv.Addr().Interface().(*int)
*vvp = 100
fmt.Println(v)

结构体中的用法

在 go 中,结构体字段的标签,也是结构体定义的一个部分。一般情况下,我们只有通过反射包,才能利用定义的这个标签。

此处我们用一段程序代码来演示相关用法。

当使用 go 语言开发 HTTP 接口时,经常需要获取客户端传递过来的接口参数,并将其映射到结构体中,此处的映射就是使用反射实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
func parseBody(req *http.Request, body any) error {
if err := req.ParseForm(); err != nil {
return err
}
// 使用一个 map 来存储字段数据,key 值为 tag
fields := make(map[string]reflect.Value)
v := reflect.ValueOf(body).Elem() // 传递进来的参数必须为指针,原因可以参考上一节的内容
for i := 0; i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i) // 字段信息
tag := fieldInfo.Tag // Tag 包含了字段的 tag 列表,这是一个 reflect.StructTag 类型
name := tag.Get("http") // 可以用 Get 方法来获取指定 tag 的值
if name == "" {
name = strings.ToLower(fieldInfo.Name) // 此处对未指定 http tag 的字段设置默认 tag
}
fields[name] = v.Field(i)
}

for name, values := range req.Form {
f := fields[name]
if !f.IsValid() {
continue // 无效的值
}
for _, value := range values {
if f.Kind() == reflect.Slice { // 切片类型,参数每出现一次会记录为切片中的一个元素
elem := reflect.New(f.Type().Elem()).Elem() // 新建一个切片基类型的指针并取其指向的值,比如说 []string 就是新建一个 *string 类型的指针并取其指向的 string,使用这种方式是为了创建一个能够修改的切片基类型变量
if err := populate(elem, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := populate(f, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
}
}
}
return nil
}

// populate 函数用来将 string 类型的参数转换为 reflect.Value 类型(仅作示范,未实现所有类型)
func populate(v reflect.Value, value string) error {
switch v.Kind() {
case reflect.String:
v.SetString(value)
case reflect.Int:
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.SetInt(i)
case reflect.Bool:
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
v.SetBool(b)
default:
return fmt.Errorf("unsupported kind %s", v.Type())
}
return nil
}

可以阅读以上代码和注释,了解一下结构体和反射结合使用的姿势。此处简单总结一下常用的几个方法:

  • NumField() int:Type 和 Value 都能调用该方法,返回其字段数量
  • func (Type)Field(i int) StructField:返回其字段信息
  • func (v Value) Field(i int) Value:返回字段的值
  • func (tag StructTag) Get(key string) string:返回指定标签的值

结语

通过反射可能方便我们编写出一些正常情况下无法实现的功能,但是使用反射是很危险的事情,在使用反射前需要反复确定反射是否是必要的。因为使用反射,你的代码可能会出现以下问题:

  1. 反射代码的错误在编译时无法检测,可能会在程序运行后甚至运行很长一段时间后才暴露出来
  2. 反射代码的可读性很差,维护的成本很高
  3. 反射代码的执行效率通常比正常代码低一到两个数量级

因此,一般建议反射只在必要的时候使用,且需要编写齐全的注释以方便后面维护该代码的程序员能读懂它。

参考链接

go 语言圣经《The Go Programming Language》中文版本