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

本文使用的 case 皆为项目开发中的实例,为了脱敏简化了代码
相信大家在使用 go 编写业务代码的时候都会写过这样的代码,这类代码的本质是,将一个集合中的数据按照名称绑定到结构体的对应属性上去,集合的类型不限于 map slice struct 甚至可以是 interface

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出现的几率也就大大增加,我们的目的就是通过反射来简化赋值的步骤,通过一个方法将集合中的数据绑定到结构体上 avatar

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

  • _type 类型信息
  • *data 指向实际值的指针
  • itab 接口方法
    图上第一个 type 是一个反射类型对象,表示了变量类型的一些信息,第二个表示结构体属性对应的的 type,包含了结构体属性的一些信息

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

avatar

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)
}

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 的编写代码。

参考与出处

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