前言

因为 golang 静态强类型语言特性以及没有很好的泛型支持导致在用 go 写 web 服务的时候,总会因为要对 http params 的解析和类型转换上要花很多时间,并且这会让代码显得很冗余,那有什么办法可以解决这一苦痛呢?答案当然是有的,这里我讲会到如何用 reflect 包写一个工具类实现 model 层 struct 与 http params 的自动映射绑定。
具体实现其实很简单,主要用到的就是通过 reflect.TypeOf() 获取字段的类型(包括字段名,类型,Tag描述等相关信息),以及 reflect.ValueOf() 来获取字段的值类型用于复写从params获取到的数据, 同时还要注意不同类型数值在 Set 时的差别。

用料

首先我们设计一个struct来储存每个反射字段的属性,就比如以下这样。
注意:取决于 golang 对于反射模型实现上的差异,这种操作在 go 里面其实并不是那么的高效,推荐在第一次反射后 cache 一份结果到内存,以便下次用的时候直接获取。


type field struct {
	name     string
	def      bool
	defValue reflect.Value
	required bool
}

通过 range reflect.Type 获取 struct field 信息并填充到 []*field ,其中这包括了字段是否必传、默认值、字段名,这些都可以利用自定义 TAG 描述实现。


type Account struct {
	Email string `params:"email;required"`
	Name  string `params:"name;required"`
	Sign  string `params:"sign"`
	San   int    `params:"sam" defalut:"-99999"`
}

除此之外我们还得需要一个 bitmap 用于映射 reflect.Kind 对应着的类型关系,以便于在 Set Value 时做好类型转换


var bitMap = map[reflect.Kind]int{
	reflect.Int.String
	reflect.Int:     32,
	reflect.Int16:   16,
	reflect.Int32:   32,
	reflect.Int64:   64,
	reflect.Int8:    8,
	reflect.Uint:    32,
	reflect.Uint16:  16,
	reflect.Uint32:  32,
	reflect.Uint64:  64,
	reflect.Uint8:   8,
	reflect.Float32: 32,
	reflect.Float64: 64,
}

实现

当完备以上材料后,想要实现我们的功能那就如鱼得水轻松自如了,只需要 range 我们定义好的 field slice,依次从 url.Values 中 Get 参数中的值,因为 http 协议的请求报文是面向文本没有额外的类型描述,因此我们获取到的数据都是文本类型,这时候我们需要根据 reflect.Kind 以不同类型调用不同 Set 方法。


func setValue(data string, v reflect.Value) (err error) {
	kind := v.Kind()
	switch kind {
	case reflect.Int64, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:
		var d int64
		d, err = strconv.ParseInt(data, 10, bitMap[kind])
		if err != nil {
			return
		}
		v.SetInt(d)
	case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8:
		var d uint64
		d, err = strconv.ParseUint(data, 10, bitMap[kind])
		if err != nil {
			return
		}
		v.SetUint(d)
	case reflect.Float64, reflect.Float32:
		var d float64
		d, err = strconv.ParseFloat(data, bitMap[kind])
		if err != nil {
			return
		}
		v.SetFloat(d)
	case reflect.String:
		if data == "" {
			return
		}
		v.SetString(data)
	return

reflect 性能优化

reflect慢主要有两个原因:一是涉及到内存分配 malloc 以后的 GC(这在Go 1.9 Vsersion 中有所改善);二是 reflect 实现里面有大量的枚举,以及类型推导之类的,再者 reflect.ValueOf()  得到的 reflect.Value 类型是一个具体的值,每次反射都得需要重新 malloc 这就又拖慢了整个过程的速度。
那如果我们不使用 reflect.ValueOf() 得到值,直接使用 reflect.TypeOf() 的结果操作 struct 的数据,省掉一层反射是不是速度就会快很多呢? 答案当然是一定的!直接上代码~


func setValueWithPointer() {
	acc := &Account{}
	tp := reflect.TypeOf(acc).Elem()
	field, _ := tp.FieldByName("Email")
	fieldPtr := uintptr(unsafe.Pointer(acc)) + field.Offset
	*((*string)(unsafe.Pointer(fieldPtr))) = "admin#otokaze.cn"
	fmt.Println(acc) // stdout: &{admin#otokaze.cn   0}
}

我们很巧妙的利用了 reflect.TypeOf() 预留给我们的 struct 内部 field 内存地址的偏移量,也因为 uintptr() 强转得到的是一个整形内存地址,这是可以进行算术运算的,只要拿到初始化 struct 后分配开始的内存地址再加上 field 内存地址的偏移量,我们就能直接拿到这个 field 在物理内存上的地址,以此来写入我们需要的内容。这种最直接的方式也节省了 reflect.ValueOf() 做的二次反射,同时也达到了我们的修改目的。
以上,由此可见只要掌握了正确的姿势,golang 的反射效率依旧可以有很大提升!反射的应用场景还远不只如此,我们都知道因为静态语言的关系在 golang 没有如同 php 中 $$ 可变变量的支持,其实也可以通过反射来实现类似的效果,不过这就不是今天这篇文章所属的范畴了,把它当作知识点,循循善诱。还有更多的技巧等着你发现~


生きて生きて生きて生きて生きて。