A few weeks ago, I was working on a Golang microservice where I needed to add support for CRUD operations with JSON data. Normally, I would make a structure for the entity with all the fields defined in it along with ‘omitempty’ attribute as shown
几周前,我正在研究Golang微服务,需要在其中添加对使用JSON数据进行CRUD操作的支持。 通常,我会为实体创建一个结构,其中定义了所有字段以及'omitempty'属性,如下所示
type Article struct {
Id string `json:"id"`
Name string `json:"name,omitempty"`
Desc string `json:"desc,omitempty"`
}
Problem
问题
But this kind of representation poses a serious problem, especially for Update or Edit operations.
但是这种表示形式带来了严重的问题,尤其是对于Update或Edit操作。
For example, let’s say that the update request JSON looks something like this
例如,假设更新请求JSON看起来像这样
{"id":"1234","name":"xyz","desc":""}
Notice the empty desc field. Now let's see how it is unmarshalled in Go
注意空的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:}
Here description comes as an empty string, It’s clearly visible that the client wants to set desc as an empty string and that is inferred by our program.
这里的描述是一个空字符串,很明显,客户端希望将desc设置为一个空字符串,这是由我们的程序推断出来的。
But what if the client does not want the change the existing value for Description, in that case, sending a big description string again is not the right thing to do, hence the JSON would look like this
但是,如果客户端不希望更改Description的现有值,在那种情况下,再次发送大的描述字符串是不正确的事情,因此JSON看起来像这样
{"id":"1234","name":"xyz"}
Let’s unmarshal it into our structure
让我们将其解组到我们的结构中
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:}
Well, we still get Desc as an empty string , so how do we differentiate between not-set field and empty field
好了,我们仍然将Desc作为空字符串获取,那么如何区分未设置字段和空字段
Short answer? Pointers
简短的答案? 指针
Solution
解
This is inspired by some existing Golang libraries like go-github. We can change our struct fields to pointer types, which would look like this
这受一些现有的Golang库(例如go-github )的启发。 我们可以将结构字段更改为指针类型,如下所示
type Article struct {
Id string `json:"id"`
Name *string `json:"name,omitempty"`
Desc *string `json:"desc,omitempty"`
}
By doing this we add an extra state to our fields. If the field does not exist in the raw JSON then the struct field will be null (nil).
通过这样做,我们向字段添加了额外的状态。 如果原始JSON中不存在该字段,则struct字段将为null( nil )。
On the other hand, if the field does exist and its value is empty, then the pointer is not null and the field contains the empty value.
另一方面,如果该字段确实存在并且其值为空,则指针不为null,并且该字段包含空值。
Note- I did not change ‘Id’ to a pointer type because it cannot have a null state, id needs to be present at all times, its similar to a database id.
注意 -我没有将' Id'更改为指针类型,因为它不能具有null状态,id必须始终存在,类似于数据库id。
Let’s try it out.
让我们尝试一下。
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)
In the first case, as the description is set to an empty string, we get a non-null pointer in Desc with an empty string value. In the second case , where the field is not-set we get a null string pointer.
在第一种情况下,由于描述设置为空字符串,因此在Desc中获得了一个非空指针,该指针具有空字符串值。 在第二种情况下,未设置字段,我们得到一个空字符串指针。
Hence we are able to differentiate between the two kinds of updates. This way works not just for strings but all the other data types including integers, nested structs, etc.
因此,我们能够区分两种更新。 这种方式不仅适用于字符串,而且适用于所有其他数据类型,包括整数,嵌套结构等。
But this approach also comes with some problems.
但是这种方法也存在一些问题。
Null Safety: Non-pointer data types have inherent null safety. Meaning, a string or int can never be null in Golang. They always have a default value. But if pointers are defined then those data types are null by default if not set manually. Hence trying to access the data of those pointers without verifying the nullability can lead to crashes in your application.
空安全性:非指针数据类型具有固有的空安全性。 这意味着在Golang中,字符串或int永远不能为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("%+v\n", req)
fmt.Printf("%s\n", *req.Desc)
}
This can be easily fixed by always checking for null pointers, but may make your code look dirty.
通过始终检查空指针可以很容易地解决此问题,但可能会使您的代码看起来很脏。
Printability: As you might have noticed in the pointer-based solution output, the value of the pointers is not printed. Instead, the hex pointer is printed which is not very useful in applications. This can also be overcome by implementing the stringer interface.
可打印性:正如您在基于指针的解决方案输出中可能已经注意到的那样,不会打印指针的值。 而是打印十六进制指针,这在应用程序中不是很有用。 这也可以通过实现纵梁接口来克服。
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
}
Appendix:
附录:
- Another solution to the above problem can be using an external library to have nullable types which provide you methods to check if they are null or not without caring about pointers. 解决上述问题的另一种方法是使用外部库具有可为null的类型,这些类型可为您提供检查它们是否为null的方法,而无需关心指针。