了解和使用golang有一段时间了,由于项目比较赶,基本是现学现卖的节奏。最近有时间会在简书上记录遇到的一些问题和解决方案,希望可以一起交流探讨。
需求
map[string]interface{}
简易实现
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无法对结构体进行遍历。
json.Marshal()json.Unmarshal()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
IDid
修改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
intfloat64reflect.Set()
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时,进行递归处理就可以实现。