Golang 中使用 JSON 时如何区分空字段和未设置字段?

592次阅读  |  发布于2年以前

几周前,我在使用 Golang 微服务,需要添加使用 JSON 数据的 CURP 操作的支持。通常,我会为实体创建一个结构体,该结构体中定义了所有字段以及 'omitempty' 属性,如下所示

type Article struct {
   Id   string      `json:"id"`
   Name string      `json:"name,omitempty"`
   Desc string      `json:"desc,omitempty"`
}

问题

但是这种表示形式带来了严重的问题,尤其对于 Update 或 Edit 操作而言.

例如,假设更新请求的 JSON 数据看起来像是这样

{"id":"1234","name":"xyz","desc":""}

注意为空的 desc 字段。现在让我们来看看这段请求数据在 Go 中解封后是怎么样的

func Test_JSON1(t *testing.T) {         
   jsonData:=`{"id":"1234","name":"xyz","desc":""}`
   req:=Article{}
   _=json.Unmarshal([]byte(jsonData),&req)
   fmt.Printf("%+v",req)
}Output:
=== RUN   Test_JSON1
{Id:1234 Name:xyz Desc:}

这里的描述是一个空字符串,很明显客户端希望将 desc 设置为空字符串,这是由我们的程序推断出来的.

但是,如果客户端不希望更改 Desc 的现有值,在这种情况下,再次发送一个描述字符串是不正确的,因此请求的 JSON 数据可能看起来像是这样

{"id":"1234","name":"xyz"}

我们解封到我们的结构体中

func Test_JSON2(t *testing.T) {         
   jsonData:=`{"id":"1234","name":"xyz"}`
   req:=Article{}
   _=json.Unmarshal([]byte(jsonData),&req)
   fmt.Printf("%+v",req)
}Output:
=== RUN   Test_JSON2
{Id:1234 Name:xyz Desc:}

额,仍然会将 Desc 作为空字符串获取,那么如何区分未设置字段和空字段

简答?指针

解决办法

受到一些现有 Golang 库的启发,如 go-github[1]. 我们可以将结构体字段更改为指针类型,如下所示

type Article struct {
   Id    string      `json:"id"`
   Name *string      `json:"name,omitempty"`
   Desc *string      `json:"desc,omitempty"`
}

通过这样做,我们在字段中添加了额外的状态。如果原始 JSON 中不存在该字段,则结构体字段将为空 (*nil*).

另一方面,如果该字段确实存在并且为空,则指针不为空,并且该字段包含空值.

注意 - 我没有将 'Id' 字段更改为指针类型,因为它不具备空状态,id 是必需的,类似数据库中的 id.

我们再尝试一下.

func Test_JSON_Empty(t *testing.T) {
   jsonData := `{"id":"1234","name":"xyz","desc":""}`
   req := Article{}
   _ = json.Unmarshal([]byte(jsonData), &req)
   fmt.Printf("%+v\n", req)
   fmt.Printf("%s\n", *req.Name)
   fmt.Printf("%s\n", *req.Desc)
}
func Test_JSON_Nil(t *testing.T) {
   jsonData := `{"id":"1234","name":"xyz"}`
   req := Article{}
   _ = json.Unmarshal([]byte(jsonData), &req)
   fmt.Printf("%+v\n", req)
   fmt.Printf("%s\n", *req.Name)
}

Output

=== RUN   Test_JSON_Empty
{Id:1234 Name:0xc000088540 Desc:0xc000088550}
Name: xyz
Desc: 
--- PASS: Test_JSON_Empty (0.00s)=== RUN   Test_JSON_Nil
{Id:1234 Name:0xc00005c590 Desc:<nil>}
Name: xyz
--- PASS: Test_JSON_Nil (0.00s)

第一种情况,由于 desc 设置为空字符串,因此我们在 Desc 获得了一个非空指针并包含一个空字符串的值。第二种情况,该字段未设置,我们得到了一个空字符串指针.

因此我们能够区分两种更新。这种方式不仅适用于字符串,而且适用于其他的所有数据类型,包括整型,嵌套结构体,等.

但是这种方法也存在一些问题.

空安全性: 非指针数据类型具备固有的空安全性。在 Golang 中这意味着字符串或整型永远不能为空。他们始终具备默认值。但是如果定义了指针,则这些数据类型在未手动设置的情况下默认为空。因此,尝试在不验证可空性的情况下访问那些指针的数据可能会导致应用程序崩溃.

# 以下代码将崩溃, 因为 desc 为空
func Test_JSON_Nil(t *testing.T) {
   jsonData := `{"id":"1234","name":"xyz"}`
   req := Article{}
   _ = json.Unmarshal([]byte(jsonData), &req)
   fmt.Printf("%+v\n", req)
   fmt.Printf("%s\n", *req.Desc)
}

通过始终检查空指针可以很容易的解决此问题,但你的代码可能会看起来会很啰嗦.

可打印性: 如在基于指针的解决方案的输出中你可能已经注意到的问题,不会打印指针的值。二十打印了指针的十六进制值,这在应用程序中没什么用。这也可以通过重新使用 stringer 接口来克服.

func (a *Article) String() string {
   output:=fmt.Sprintf("Id: %s ",a.Id)
   if a.Name!=nil{
   output+=fmt.Sprintf("Name: '%s' ",*a.Name)
   }
   if u.Desc!=nil{
   output+=fmt.Sprintf("Desc: '%s' ",u.Desc)
   }
   return output
}

附录:

参考资料

[1]go-github: https://github.com/google/go-github

[2]github.com/guregu/null: https://github.com/guregu/null

[3]github.com/google/go-github: https://github.com/google/go-github

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8