背景
最近做项目时,需要和外部服务进行数据交互,但是双方的数据结构不同(虽然表示的意思相近,例:本系统的 Name
对应其 UserName
)。这时访问外部系统时,需要进行数据转换。
第一版 直接转换
直接提供方法,转成相应的结构,如下:
type UserInfo struct {
Name string
Age int
Height int
}
func (u *UserInfo) ToOutside() map[string]interface{} {
res := make(map[string]interface{})
res["UserName"] = u.Name
res["Age"] = u.Age
res["Height"] = u.Height
return res
}
这种方式通俗易懂,但是每当字段变更时,都得再手动改代码,比较麻烦。而且多人协作的时候,数据结构的变更和转换可能并不是同一个人负责的(A 负责数据变更,B 负责对接外部系统),这样又增加了沟通成本,优化下。
第二版 使用 tag 进行转换
go
中的 tag
,在很多地方都有见过,例如 json, xml
的编码,orm
的交互等,这个和我目前遇到的场景很相似,研究一下。
上述的实际应用中,tag
的转换,其核心原理都是使用反射,可以翻翻源码看下相关实现。接下来看一个简单的通过反射获取 tag
值的例子,
我们先给结构体 UserInfo
加上 tag
。
type UserInfo struct {
Name string `tag:"UserName"`
Age int `tag:"Age"`
Height int `tag:"Height"`
}
func tTagBase() {
u := UserInfo{"张三", 33, 183}
v := reflect.ValueOf(u)
for i := 0; i < v.NumField(); i++ {
fmt.Println(v.Type().Field(i).Tag)
tagValue, ok := v.Type().Field(i).Tag.Lookup("tag")
fmt.Println(tagValue, ok)
}
// tag:"UserName"
//UserName true
//tag:"Age"
//Age true
//tag:"Height"
//Height true
}
这里主要是对 reflect
包的使用,这边不做详细的介绍了。下面展示成品,加上了 extends
及 utn
的结构体字段赋值处理:
func (u *UserInfo) ToOutsideV2() map[string]interface{} {
res := make(map[string]interface{})
getMapByTag(reflect.ValueOf(u), res, tagName)
return res
}
const tagName = "tag"
const (
extends = "extends" // 对结构体使用,会延伸到对应的结构体中,将结构体中的字段赋值到最外层
useTagName = "utn" // 对结构体使用,保留原始的结构嵌套
)
func getMapByTag(v reflect.Value, res map[string]interface{}, tagName string) {
defer func(){
if err := recover(); err != nil {
fmt.Println("getMapByTag err: ", err)
}
}()
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return
}
for i := 0; i < v.NumField(); i++ {
structField := v.Type().Field(i)
if structField.PkgPath != "" { // 小写的不导出
// reflect.Value.Interface: cannot return value obtained from unexported field or method
continue
}
valueField := v.Field(i)
tag, ok := structField.Tag.Lookup(tagName)
if !ok {
continue
}
var mapKey string
var mapV interface{}
var extend bool // 结构体嵌套,放入最外层的map中
tagSlice := strings.Split(tag, ",")
for _, t := range tagSlice {
if t == extends {
getMapByTag(valueField, res, tagName)
extend = true
break
} else if t == useTagName{
// 声明嵌套的map
embeddedMap := make(map[string]interface{})
getMapByTag(valueField, embeddedMap, tagName)
mapV = embeddedMap
} else {
mapKey = t
}
}
if extend {
continue
}
if mapKey == "" {
continue
}
if mapV != nil {
res[mapKey] = mapV
continue
}
// 这里只做了结构体切片,其他的不管,直接放入mapV
if valueField.Kind() == reflect.Slice {
if valueField.Len() != 0 {
var sliceDataValue = valueField.Index(0) // 取第一个,拿它的 type
if sliceDataValue.Kind() == reflect.Ptr && !sliceDataValue.IsNil() {
sliceDataValue = sliceDataValue.Elem()
}
if sliceDataValue.Kind() == reflect.Struct {
newSlice := make([]map[string]interface{}, valueField.Len())
for i := 0;i < valueField.Len(); i++ {
oneMap := make(map[string]interface{})
getMapByTag(valueField.Index(i), oneMap, tagName)
newSlice[i] = oneMap
}
mapV = newSlice
} else {
mapV = valueField.Interface()
}
} else {
mapV = valueField.Interface()
}
} else {
mapV = valueField.Interface()
}
res[mapKey] = mapV
}
}
如果有其他的外部系统值需要转换,可以直接添加相应的标签及转换方法,如下,添加了 lab
的转换:
type UserInfo struct {
Name string `tag:"UserName" lab:"user_name"`
Age int `tag:"Age" lab:"user_age"`
Height int `tag:"Height" lab:"user_height"`
Manager Manager `tag:"Manager,extends" lab:"manager,extends"`
Manager1 Manager `tag:"Manager1,utn" lab:"manager1,utn"`
}
type Manager struct {
Office string `tag:"Office" lab:"office"`
Title string `tag:"Title" lab:"title"`
}
func (u *UserInfo) ToLab() map[string]interface{} {
res := make(map[string]interface{})
getMapByTag(reflect.ValueOf(u), res, "lab")
return res
}
func tUserToOutside() {
u := UserInfo{Name: "张三", Age: 33, Height: 183}
u.Manager = Manager{"逛逛大厦", "逛逛总监"}
u.Manager1 = Manager{"逛逛大厦", "逛逛总监"}
res := u.ToLab()
fmt.Println(res)
// map[manager1:map[office:逛逛大厦 title:逛逛总监] office:逛逛大厦 title:逛逛总监 user_age:33 user_height:183 user_name:张三]
}
总结
本篇主要是对 go
的 tag
相关内容作了个简单的介绍,作为数据转换的方式使用时,效率是没有直接转换高的,但是胜在改动方便,需要注意的是,上述 getMapByTag
方法并未适配所有的数据类型,如有需要,需自行适配。
相关代码在我的 gitee仓库,具体代码在 GoTest/reflectTest/tag.go