go 自定义 tag 的使用(外部服务的结构体数据转换,使用 tag 和 reflect 转成对应的 map)

背景

最近做项目时,需要和外部服务进行数据交互,但是双方的数据结构不同(虽然表示的意思相近,例:本系统的 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 包的使用,这边不做详细的介绍了。下面展示成品,加上了 extendsutn 的结构体字段赋值处理:

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:张三]
}

总结

本篇主要是对 gotag 相关内容作了个简单的介绍,作为数据转换的方式使用时,效率是没有直接转换高的,但是胜在改动方便,需要注意的是,上述 getMapByTag 方法并未适配所有的数据类型,如有需要,需自行适配。

相关代码在我的 gitee仓库,具体代码在 GoTest/reflectTest/tag.go

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值