乐趣区

关于golang: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:<nil>}
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

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

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

退出移动版