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

微信搜索 “吴亲强的深夜食堂”,关注公众号,回复 "微信",加好友拉群一起学习。

几周前,我正在开发一个基于 Golang 的微服务项目,需要在其中添加对 JSON 数据的 CRUD 支持。

通常,我会为实体构建一个结构,其中包含定义的所有字段以及 “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 设置为空字符串,这是由我们的程序推断出来的。

但是,如果客户端不希望更改 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。我们可以将结构体字段修改为指针类型,

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

通过这样做,我们向字段添加了一个额外的状态。如果这个字段在原始请求 JSON 数据中不存在,那么字段将会为 null(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("%+vn", req)
   fmt.Printf("%sn", *req.Name)
   fmt.Printf("%sn", *req.Desc)
}
func Test_JSON_Nil(t *testing.T) {
   jsonData := `{"id":"1234","name":"xyz"}`
   req := Article{}
   _ = json.Unmarshal([]byte(jsonData), &req)
   fmt.Printf("%+vn", req)
   fmt.Printf("%sn", *req.Name)
}

输出,

=== 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:}
Name: xyz
--- PASS: Test_JSON_Nil (0.00s)

在第一种情况下,当 desc 被设置成空字符串,我们得到一个非空指针,它的值是空字符串。在第二种情况下,如果字段没有被设置,我们得到一个空指针。

因此,我们就能够区分这两种更新了。这种方法不仅适用于字符串类型,还适用于其他类型,包括 int、嵌套 struct 等。

但是这种方法会带来一些问题。

空安全:非指针数据类型具有固有的空安全。这更确切的说,一个 string 或者 int 在 Golang 中永远不会为 null。它们总是有一个默认值。但是,一旦定义了指针,如果没有手动设置,那么这些数据类型默认为 null。因此,尝试访问这些指针数据而不验证空性的情况下可能会导致应用程序崩溃。

#The following code will crash because desc is null
func Test_JSON_Nil(t *testing.T) {
   jsonData := `{"id":"1234","name":"xyz"}`
   req := Article{}
   _ = json.Unmarshal([]byte(jsonData), &req)
   fmt.Printf("%+vn", req)
   fmt.Printf("%sn", *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 a.Desc!=nil{
   output+=fmt.Sprintf("Desc: '%s' ",a.Desc)
   }
   return output
}

附录
另一个解决方案是使用外部库来拥有可为空的类型,这些类型提供了检查它们是否为空的方法,而不需要关心指针。

https://github.com/guregu/null

https://github.com/google/go-github

是不是很实用?你还有别的解决方案嘛?欢迎下方留言一起讨论

如果文章对你有所帮助,点赞、转发、留言都是一种支持!

你可能感兴趣的:(golang)