golang 通过reflect遍历struct并赋值 & 自动创建struct

了解和使用golang有一段时间了,由于项目比较赶,基本是现学现卖的节奏。最近有时间会在简书上记录遇到的一些问题和解决方案,希望可以一起交流探讨。

需求

  • 在golang中,给定一组数据,例如 map[string]interface{}
    类型的数据,创建一个对应的struct并赋值

简易实现

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16.25,
}

type Fruit struct {
    ID    int
    Name  string
    Price float64
}

func newFruit(data map[string]interface{}) *Fruit {
    s := Fruit{
        ID:    data["id"].(int),
        Name:  data["name"].(string),
        Price: data["price"].(float64),
    }
    return &s
}

func main() {
    fruit := newFruit(data)
    log.Println("fruit:", fruit)
}

> fruit: &{1001 apple 16.25}

这样实现简单快速,但也有缺点:

  • 难以维护,每次新增字段都要修改newFruit函数
  • 不够优雅,需要手动对每一个字段进行赋值和类型转换
  • 不够通用,只能创建钦定的struct

改进

是否有更好的解决方法,自动遍历struct对象,并进行赋值呢?

首先想到for…range操作符,但golang里range无法对结构体进行遍历。

(如果只需遍历struct而不用赋值,可以尝试邪道组合: json.Marshal()
json.Unmarshal()
一键把struct转成 map[string]interface()
)

实际上要遍历一个struct,需要使用golang的reflect包。关于golang的反射机制不再赘述,可以参考go的文档,有很详细的说明。
那么现在利用reflect,尝试改进之前的代码

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16.25,
}

type Fruit struct {
    ID    int
    Name  string
    Price float64
}

// 遍历struct并且自动进行赋值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 传入的inStructPtr是指针,需要.Elem()取得指针指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍历结构体
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        if v, ok := data[t.Name]; ok {
            f.Set(reflect.ValueOf(v))
        } else {
            panic(t.Name + " not found")
        }
    }
}
func main() {
    //fruit := newFruit(data)
    fruit := Fruit{}
    structByReflect(data, &fruit)
    log.Println("fruit:", fruit)
}
编译运行
> panic: ID not found

新的问题出现了,结构体的字段名 ID
和data中的 id
大小写不一致,导致无法从data中取得对应的数据。

修改data的key name,或者修改struct的field name当然可以解决,但在实际应用中,data往往从外部获得不受控制,而data的key通常也不符合go的命名规范,因此暴力改名不可取。

那怎么解决呢?这里可以利用go的 成员变量标签(field tag)
,给struct的字段增加额外的元数据,用以指定对应的字段名。golang对json和xml等的序列化处理也是用了这个方法。

type Fruit struct {
    ID    int     `key:"id"`
    Name  string  `key:"name"`
    Price float64 `key:"price"`
}
// 遍历struct并且自动进行赋值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 传入的inStructPtr是指针,需要.Elem()取得指针指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍历结构体
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        // 得到tag中的字段名
        key := t.Tag.Get("key")
        if v, ok := data[key]; ok {
            f.Set(reflect.ValueOf(v))
        } else {
            panic(t.Name + " not found")
        }
    }
}

再次编译运行,这次得到了期望的结果

> fruit: {1001 apple 16.25}

类型转换问题

到这里已经基本实现了想要的功能,但还有一个问题,如果data中的数据类型,和struct中定义的类型稍有不一致,反射赋值语句就会报错,

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16,  // 改成int类型
}

测试一下:

> panic: reflect.Set: value of type int is not assignable to type float64

我们知道 int
float64
可以相互强制转换,但是 reflect.Set()
方法并不想帮你转。

这里还是要利用reflect包的两个方法, Type.ConvertibleTo(u Type)
用来判断能否转换到指定类型,再通过 Value.Convert(t Type)
来进行类型转换。
再次优化我们的函数:

// 遍历struct并且自动进行赋值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 传入的inStructPtr是指针,需要.Elem()取得指针指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍历结构体
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        // 得到tag中的字段名
        key := t.Tag.Get("key")
        if v, ok := data[key]; ok {
            // 检查是否需要类型转换
            dataType := reflect.TypeOf(v)
            structType := f.Type()
            if structType == dataType {
                f.Set(reflect.ValueOf(v))
            } else {
                if dataType.ConvertibleTo(structType) {
                    // 转换类型
                    f.Set(reflect.ValueOf(v).Convert(structType))
                } else {
                    panic(t.Name + " type mismatch")
                }
            }
        } else {
            panic(t.Name + " not found")
        }
    }
}

在f.Set()之前,先检查data的Type和struct字段的Type是否一致,如果不一致则进行转换。

> fruit: {1001 apple 16}

这样功能就全部完成了,示例代码中遇到错误都直接抛出panic,可以根据实际项目进行调整。
主要到这里没有处理嵌套的结构体等情况,这部分通过判断Type为struct时,进行递归处理就可以实现。

完整代码:
GitHub