了解和使用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
有疑问加站长微信联系(非本文作者)