Reflection in Struct tag

golang中的struct类型,在使用的时候为了编码方便我们经常会给其内部的field指定一些特定的tag。这些tag不仅仅可以用来改变编码后相应字段名称,还有一些特别的作用。比如在使用Json这种数据格式的时候,我们可以为struct的field指定一个自定义的json tag,在按照json的格式编码转换的时候,该field的名字就会使用我们自定义的,而不是采取默认的将field的名字的小写形式。如果此时再加上一些特殊的tag,会改变编码时候的行为。如omiempty,在编码一个struct的时候,只要有field值为0值的时候,都不会把他们encoding到json当中。

在reflect中,我们同样可以通过strct的tag以及fieldName等属性对一个未知的struct进行操作。

Extract params to appropriate Type

在使用golang实现一些网络接口的时候,普遍需要的一项工作就是解析参数。将http请求带过来的参数解析到之前约定好的数据类型的变量内。这些变量一般都是struct类型,毕竟golang中是通过struct来实现类型的组合,以便构造自定义类型的。那么实现一个这样的结构需要哪几步呢?

  1. 获取http传递进来的所有参数,无论是get还是post
  2. 定义好包含所有要处理参数的struct类型
  3. 用解析到http请求参数的字段名通过反射获取到对应struct类型中与其tag相等的field
  4. 将之前从请求中解析出的参数值赋值给已经找到的field

第一步完成比较容易,使用http包中的ParseForm即可将请求中传递的参数都放在req.Form中。第二步也同样简单,我们只需要将要接收的参数以及其对应的类型定义到一个struct内就行了。但是要注意的是,每一个字段tag名称都必须要和请求参数中对应的字段名相等才可以,不然我们就没法通过请求参数的字段名和将要填充的Struct类型的变量链接在一起了。整个解析参数的功能最重要的就是第三步。

我们可以在外循环遍历请求参数中的字段名,内循环遍历定义好的struct类型变量的field。但是这样效率是非常低的。稍微考虑一下就可以将定义好的struct类型的变量保存在一个map[string]reflect.Value内,map的key为struct内对应的tag,value则为对应的字段值。这样我们就可以在拿到请求参数字段名的时候,以O(1)的复杂度在这个map内找到其对应的field值,然后调用相应的Set方法即可将请求参数的值解析到具体类型的变量中。之所以能够通过构造出来的map改动定义好的struct类型变量内的field值,是因为map本身是一个引用类型,其底层使用的内存空间还是属于struct。

实现代码如下

 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
62
import (
	"fmt"
	"github.com/qiniu/errors"
	"net/http"
	"reflect"
	"strconv"
)

func ExtractHttpReq(req *http.Request, ptr interface{}) error {
	m := make(map[string]reflect.Value)
	value := reflect.ValueOf(ptr).Elem()
	if value.Kind() != reflect.Struct || !value.IsValid() {
		return errors.New("input data is invalid")
	}

	for i := 0; i < value.NumField(); i++ {
		tag := value.Type().Field(i).Tag.Get("http")
		if tag == "" {
			tag = value.Type().Field(i).Name
		}
		m[tag] = value.Field(i)
	}

	if err := req.ParseForm(); err != nil {
		return err
	}

	for key, value := range req.Form {
		f, ok := m[key]
		if !ok || !f.IsValid() {
			continue
		}

		for _, item := range value {
			Populate(f, item)
		}
	}

	return nil
}

func Populate(value reflect.Value, item string) {
	switch value.Kind() {
	case reflect.Slice:
		vv := reflect.New(value.Type().Elem()).Elem()
		Populate(vv, item)
		value.Set(reflect.Append(value, vv))
	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Uint64:
		num, _ := strconv.ParseInt(item, 10, 64)
		value.SetInt(num)
	case reflect.Bool:
		num, _ := strconv.ParseBool(item)
		value.SetBool(num)
	case reflect.String:
		value.SetString(item)
	default:
		fmt.Println("unsupport type")
	}

	return
}

Reflection in method

除了上面讲到的,可以使用reflect通过struct tag找到其对应的field变量并且更新里面的值之外。我们还可以是用reflect通过一个普通类型的值,找到其定义的成员函数。

 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
package main

import (
	"fmt"
	"reflect"
	"strings"
)

type T struct {
	Text string
}

func (t T) GetText() string {
	fmt.Println("get text")
	return t.Text
}

func PrintMethod(v reflect.Value) {
	if !v.IsValid() {
		fmt.Println("input params is invalid")
		return
	}

	for i := 0; i < v.NumMethod(); i++ {
		fmt.Printf("func (%s) %s %s\n", v.Type().String(),
			v.Type().Method(i).Name, strings.TrimPrefix(v.Method(i).Type().String(), "func"))
		fmt.Println(v.Method(i).Call([]reflect.Value{}))
	}

	return
}

func main() {
	a := T{Text: "haha"}
	PrintMethod(reflect.ValueOf(a))
	return
}

通过上面的实现我们可以看出来,reflect.Type以及reflect.Value都有自己的Method方法。只不过reflect.Type的Method返回的是一个Method类型的数据,它里面包含了关于这个method的一切信息。但是reflect.Value的Method返回的是一个包含了函数值的reflect.Value。我们可以直接通过reflect.Value.Method(index).Call的形式去调用这个函数。

How to use reflect

使用reflect包写出来的代码实际上都是非常脆弱的,尽管我们在使用的过程当中能够尽量的保证我们的程序不panic。但是仔细想想,reflect包一般被用于实现我们的标准库,标准库内对painic的发生是有一套健全的处理机制的,而我在日常工作当中的项目里还仅仅只是处理error而不是处理panic。这可能跟我写的代码都是上层逻辑有关,如果实现一些标准库的话,可能就需要去注意panic的处理了。并且,reflect包使用过程当中的错误,基本不会被在编译时期就检查出来,都是在运行时触发的。

reflect.Value类型的变量在调用相应方法的时候,一定记得先要检查这个变量是否是zero value或者它的动态类型是什么。因为reflect包中的很多方法在调用的时候都不会帮你做这些,一旦你使用了不当的类型调用了其所没有实现的方法的时候,将会直接导致panic的发生。另外一个类似的问题就是,在调用setxx方法之前,要检查一下这个变量是否是可寻址的并且是可以更新的。

还有一个比较危险的地方就是,reflect包无法识别无限递归的问题。我们之前实现了一个格式化输出一般数据类型变量内容的函数,假设这个数据类型的内部成员的类型和外部是一致的,就会导致无限递归的调用。如:

1
2
3
4
type Recursive struct {
	T Recursive
	A string
}

这种例子其实并不罕见,比如链表的实现常常就会使用类似的数据结构。而且reflect的代码因为没有依赖具体的数据类型,所以它的速度是非常慢的。