在 go 语言中利用反射精简代码

本文介绍了如何利用Go语言的反射特性来简化代码,特别是处理结构体赋值的情况。通过一个业务开发实例,解释了如何创建一个绑定函数,利用反射和结构体标签将数据从集合(如map或slice)绑定到结构体属性上,以此减少冗长的手动赋值代码。同时提到了结构体参数校验、ORM工具等实际开发中应用反射的场景。

反射是 Go 语言中非常重要的一个知识点。反射是设计优雅程序的法宝,orm json 序列化,参数校验都离不开它,我们今天以一个业务开发中的实例,来简单讲解下反射在日常开发中的用处。

本文使用的 case 皆为项目开发中的实例,为了脱敏简化了代码

相信大家在使用 go 编写业务代码的时候都会写过这样的代码,这类代码的本质是,将一个集合中的数据按照名称绑定到结构体的对应属性上去,集合的类型不限于 map slice struct 甚至可以是 interface[^interface],

[^interface]: 这个就比较 trick 了

type TestValue struct {
        IntValue int
        StringValue string
        IntArray   []int
        StringArray []string
    }

    dataMap := map[string]string{
        "int_value":"1",
        "string_value":"str",
        "int_array":"[1,2,3]",
        "string_array":"[\"1\",\"2\",\"3\"]",
    }

    config := TestValue{}

    if value, ok := dataMap["int_value"]; ok {
        config.IntValue, _ = datautil.TransToInt64(value)
    }
    if value, ok := dataMap["string_value"]; ok {
        config.StringValue = value
    }
    if value, ok := dataMap["int_array"]; ok {
        config.IntArray = stringToIntArray(value)
    }
    if value, ok := dataMap["string_array"]; ok {
        config.StringArray = stringToStrArray(value)
    }

    return config

这部分代码中最挫的地方就是结构体赋值的时候一个一个进行的复制,若整个结构体非常大,赋值的代码可能会写满满一屏,bug出现的几率也就大大增加,我们的目的就是通过反射来简化赋值的步骤,通过一个方法将集合中的数据绑定到结构体上


v2-a16814d4b3c2ea47cec720c20b1a48b7_b.jpg


反射简述

要做到这一步,我们首先了解下,在 go 语言中,我们的变量是由什么组成的

  • _type 类型信息
  • *data 指向实际值的指针
  • itab 接口方法

图上第一个 type 是一个反射类型对象,表示了变量类型的一些信息,第二个表示结构体属性对应的的 type,包含了结构体属性的一些信息

reflect.Type : /go/src/reflect/value.go:36


v2-5eb7ceb05a95635ae9e9c4cddf561633_b.jpg


bind function

看到这张图我们大概就明白应该怎样做了,目标是编写一个绑定方法,必须建立一个绑定关系,把这个结构体加上一个 tag ,通过 tag 和 map 中的数据建立关联

// 创建一个具有深刻含义的 tag
    type TestValue struct {
        IntValue     int            `qiudianzan:"int_value"`        
        StringValue string        `qiudianzan:"string_value"`
        IntArray       []int        `qiudianzan:"int_array"`
        StringArray []string    `qiudianzan:"string_array"`
    }

紧接着获取结构体的 tag ,通过反射轻而易举的做到了

func bind(configMap map[string]string, result interface{}) error {

    v := reflect.ValueOf(result).Elem()
    t := v.Type()

    for i := 0; i < t.NumField(); i++ {
        tag := t.Field(i).Tag.Get("qiudianzan")
        fmt.Println(tag)
    }

绑定关系完成以后,就需要向结构体写入 map 中的值,这时候问题来了,map 中的数据结构都是 string ,但是结构体属性类型五花八门,并不能直接将 map 中的数据写入,还需要根据结构体属性类型做一步类型转换

v.Field(i).SetInt(res)
    v.Field(i).SetUint(res)
    v.Field(i).SetFloat(res)
    .
    .
    .

通过反射可以获取属性的两种表示类型的反射对象

reflect.Type    // 静态类型
    reflect.Kind    // 底层数据的类型

我们通过下面的例子来确定使用哪一个

type A struct {
    }

    func main() {
        var a A
        kinda := reflect.ValueOf(a).Kind()
        typea := reflect.TypeOf(a)

        fmt.Println(kinda)
        fmt.Println(typea)
    }
struct
    main.A

变量 a 的静态类型为 A,但是 a 的底层数据类型则为 struct,所以我们想根据类型解析,这里说的类型是指的 reflect.Kind

通过底层数据类型来转换 map 中的数据

switch v.Field(i).Kind() {

        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            res, err := strconv.ParseInt(value, 10, 64)
            if err != nil {
                return err
            }
            v.Field(i).SetInt(res)

        case reflect.String:
            v.Field(i).SetString(value)

        case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
            res, err := strconv.ParseUint(value, 10, 64)
            if err != nil {
                return err
            }
            v.Field(i).SetUint(res)

再稍稍整理下添加一点细节,一个简单的绑定函数就大功告成了

func bind(configMap map[string]string, result interface{}) error {

    // 被绑定的结构体非指针错误返回
    if reflect.ValueOf(result).Kind() != reflect.Ptr {
        return errors.New("input not point")
    }

    // 被绑定的结构体指针为 null 错误返回
    if reflect.ValueOf(result).IsNil() {
        return errors.New("input is null")
    }

    v := reflect.ValueOf(result).Elem()
    t := v.Type()

    for i := 0; i < t.NumField(); i++ {
        tag := t.Field(i).Tag.Get("json")

        // map 中没该变量有则跳过
        value, ok := configMap[tag]
        if !ok {
            continue
        }

        // 跳过结构体中不可 set 的私有变量
        if !v.Field(i).CanSet() {
            continue
        }

        switch v.Field(i).Kind() {
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            res, err := strconv.ParseInt(value, 10, 64)
            if err != nil {
                return err
            }
            v.Field(i).SetInt(res)
        case reflect.String:
            v.Field(i).SetString(value)
        case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
            res, err := strconv.ParseUint(value, 10, 64)
            if err != nil {
                return err
            }
            v.Field(i).SetUint(res)
        case reflect.Float32:
            res, err := strconv.ParseFloat(value, 32)
            if err != nil {
                return err
            }
            v.Field(i).SetFloat(res)
        case reflect.Float64:
            res, err := strconv.ParseFloat(value, 64)
            if err != nil {
                return err
            }
            v.Field(i).SetFloat(res)
        case reflect.Slice:
            var strArray []string
            var valArray []reflect.Value
            var valArr reflect.Value
            elemKind := t.Field(i).Type.Elem().Kind()
            elemType := t.Field(i).Type.Elem()

            value = strings.Trim(strings.Trim(value, "["), "]")
            strArray = strings.Split(value, ",")

            switch elemKind {
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                for _, e := range strArray {
                    ee, err := strconv.ParseInt(e, 10, 64)
                    if err != nil {
                        return err
                    }
                    valArray = append(valArray, reflect.ValueOf(ee).Convert(elemType))
                }
            case reflect.String:
                for _, e := range strArray {
                    valArray = append(valArray, reflect.ValueOf(strings.Trim(e, "\"")).Convert(elemType))
                }
            }

            valArr = reflect.Append(v.Field(i), valArray...)
            v.Field(i).Set(valArr)
        }
    }

    return nil
}

之前的看起来非常难看的代码瞬间就变得很简单

type TestValue struct {
        IntValue int
        StringValue string
        IntArray   []int
        StringArray []string
    }

    dataMap := map[string]string{
        "int_value":"1",
        "string_value":"str",
        "int_array":"[1,2,3]",
        "string_array":"[\"1\",\"2\",\"3\"]",
    }

    config := TestValue{}

    err := bind(dataMap,&config)

在这里只是提供一种绑定的思路,其实在实际开发中,遇到结构体值绑定/校验/格式化/方法绑定,都可以使用类似的思路,避免 one by one 的编写代码

这是使用这种思路的一些开源工具

  1. 结构体参数校验 gopkg.in/go-playground/
  2. 格式化 json.Unmarshal
  3. orm工具 github.com/jmoiron/sqlx 将结构体属性转换成 pre sql

作者:许睿


滴滴云-为开发者而生 滴滴云使者

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值