前言
因为 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 中 $$
可变变量的支持,其实也可以通过反射来实现类似的效果,不过这就不是今天这篇文章所属的范畴了,把它当作知识点,循循善诱。还有更多的技巧等着你发现~
有疑问加站长微信联系(非本文作者)