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

gazellecon · · 7269 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

了解和使用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

我们知道intfloat64可以相互强制转换,但是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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:gazellecon

查看原文:golang 通过reflect遍历struct并赋值 & 自动创建struct

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

7269 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传