前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Go系列:如何在不修改结构体定义的情况下支持新增字段

Go系列:如何在不修改结构体定义的情况下支持新增字段

原创
作者头像
用户9805946
修改2024-11-26 19:58:08
修改2024-11-26 19:58:08
1290
举报
文章被收录于专栏:Go

背景

在go中对api请求,一般是先定义一个结构体,然后执行http请求,再使用json.Unmarshal将返回的body反序列化到结构体实例中。比如我们实现一个cmd,执行API查询资源然后显示在终端。

但是在一些场景下,API返回的结构体会因为特性变动而变化,比如新增特性导致返回的结构体中的字段变多,如果不随之修改结构体定义,那么我们使用该结构体时就会导致丢失新增数据。每次取修改结构体有时候也不是特别方便,例如在命令工具中只是简单的显示此字段值,没有必要每次都去修改命令,而且倒是服务间的耦合。

比如如下例子

代码语言:go
复制
type User struct {
    Name  string                 `json:"name"`
    Age   int                    `json:"age"`
}

func main() {
	jsonData := `{"user":{
		"name": "张三",
		"age": 18,
		"location": "北京市"
	}}`

	var s struct {
		User *User `json:"user"`
	}

	if err := json.Unmarshal([]byte(jsonData), &s); err != nil {
		fmt.Println("Error:", err)
		return
	}
	user := s.User

	fmt.Printf("user: %+v\n", user)
}

表示user的字符串中,user结构体还有location字段,但是我们实际反序列化出来后,这个字段的值丢失了,要想保留localtion字段,就需要在user中新增一个location字段,但是如果下次再增加gender字段,那么就又要修改user结构体

解决

我们可以用接下来介绍的方法解决这个问题。

代码语言:go
复制
type User struct {
	Name  string                 `json:"name"`
	Age   int                    `json:"age"`
	Extra map[string]interface{} `json:"-"`
}

func (u *User) UnmarshalJSON(data []byte) error {
	type Proxy User

	if err := json.Unmarshal(data, (*Proxy)(u)); err != nil {
		return err
	}

	// 解析原始JSON数据,捕获所有未定义的字段
	if err := json.Unmarshal(data, &u.Extra); err != nil {
		return err
	}

	var fields []string
	rv := reflect.ValueOf(User{})
	for i := 0; i < rv.NumField(); i++ {
		tag := rv.Type().Field(i).Tag.Get("json")
		if tag == "" || tag == "-" {
			continue
		}
		fields = append(fields, tag)
	}

	// 将未定义的字段放入Extra字段
	for _, field := range fields {
		delete(u.Extra, field)
	}

	return nil
}

func main() {
	jsonData := `{"user":{
		"name": "张三",
		"age": 18,
		"gender": "male",
		"location": "北京市"
	}}`

	var s struct {
		User *User `json:"user"`
	}

	if err := json.Unmarshal([]byte(jsonData), &s); err != nil {
		fmt.Println("Error:", err)
		return
	}
	user := s.User

	fmt.Printf("user: %+v\n", user)
}

在上面的例子中,我们在user结构体中定义了一个Extra字段,类型为mapstringany,用它来保存所有未定义的字段和值。

接下来,我们实现了user的UnmarshalJSON方法,这个方法理解起来也比较容易,就是

  1. 先执行json.Unmarshal, 对user实例进行反序列化,那么未在user结构体中定义的field就丢失了
  2. 在对user.Extra进行反序列化,这里因为Extra类型是mapstringany, 那么所有的key和value都会保存在这个map中
  3. 接下来我们通过获取user结构体的json tag,获取结构体字段在map中key
  4. 将上述key从u.Extra中删除,就得到了未在User中定义的key和val

那么在对user对象调用Unamarshal时,所有未在User中写明的属性都保存在Extra中了,后续的使用就可以从Extra总获取了。

注意

UnmarshalJSON中,有如下关键的一行

代码语言:go
复制
	type Proxy User

	if err := json.Unmarshal(data, (*Proxy)(u)); err != nil {
		return err
	}

为什么这里要先定一个Use类型别名,而且在json.Unmarshal中要将u转换成(*Proxy)呢

是为了防止递归调用导致死循环。应为如果直接如下调用, 那么这里就会右走到user的UnmarshalJSON方法了。

代码语言:go
复制
	if err := json.Unmarshal(data, u); err != nil {
		return err
	}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 解决
  • 注意
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档