37

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

 4 years ago
source link: https://www.tuicool.com/articles/UBvIzqM
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK