1. 概述
Golang中,巨大的坑就是struct的序列化和反序列化。
struct的字段初始值,是Go零值,例如0、""、false。在CRUD操作中,需要两次序列化和反序列化,json<-->struct<-->db,存在的问题:
- 1)增加实体时,某些字段选填,对应的内容应该是nil,不应该是""或0
- 2)更新实体时,如果要支持部分字段更新,拿到的是一个完整的struct,字段值为0或"",无法确认是否是用户填写
- 3)读取实体时,数据库的某些Number类型字段为null,读取到struct后则为0,显然不符合实际情况(0也是有意义的数值)
基于此,简单方法,只支持全量更新,且不区分零值和nil,会带来诸多不便。
如果要区分,解决办法有几个:
- 通过指针的方式解决,即将field类型定义为
*int
、*string
等,可参考package null - 自定义类型,完成struct与json以及数据库的序列化和反序列化,可以严格按照自己的想法实现
本文将对上述两种方法举例说明。
当然,也有其他怪招,比如
- 每次update请求,客户端指明
要更新的字段名
,基于此,服务端可以只读取特定字段,挺麻烦 - 将
json数据
反序列化到map[any]any
,然后逐个字段判断,这样基本上废弃了struct,代码复杂度骤增
2. 通过指针的方式
- 代码
package main
import (
"encoding/json"
"log"
)
type Foo struct{ Val *int }
func do(bytes []byte) (Foo, error) {
var a Foo
err := json.Unmarshal(bytes, &a)
return a, err
}
func testDeserialize() {
notSet := []byte(`{}`)
setNull := []byte(`{"val": null}`)
setValid := []byte(`{"val": 123}`)
setWrongType := []byte(`{"val": "123"}`)
a, err := do(notSet)
log.Printf("NotSet |value:%v |err: %v\n", a.Val, err)
a, err = do(setNull)
log.Printf("SetNull |value:%v |err: %v\n", a.Val, err)
a, err = do(setValid)
log.Printf("SetValid |value:%d |err: %v\n", *a.Val, err) // 注意:实际应用时,需要先判断a.Val是否为nil
a, err = do(setWrongType)
log.Printf("SetWrongType|value:%d |err: %v\n", *a.Val, err) // 注意,这时候a.Val居然是0,而不是nil
}
func testSerialize() {
notSet := Foo{}
setNull := Foo{nil}
setValid := Foo{&[]int{22}[0]}
res, err := json.Marshal(notSet)
log.Printf("NotSet |result:%s |err: %v\n", res, err)
res, err = json.Marshal(setNull)
log.Printf("setNull |result:%s |err: %v\n", res, err)
res, err = json.Marshal(setValid)
log.Printf("setValid |result:%s |err: %v\n", res, err)
}
func main() {
log.Println("deserialize test ...")
testDeserialize()
log.Println("serialize test ...")
testSerialize()
}
- 运行结果
% go run test_null.go
2023/05/10 15:10:12 deserialize test ...
2023/05/10 15:10:12 NotSet |value:<nil> |err: <nil>
2023/05/10 15:10:12 SetNull |value:<nil> |err: <nil>
2023/05/10 15:10:12 SetValid |value:123 |err: <nil>
2023/05/10 15:10:12 SetWrongType|value:0 |err: json: cannot unmarshal string into Go struct field Foo.Val of type int
2023/05/10 15:10:12 serialize test ...
2023/05/10 15:10:12 NotSet |result:{"Val":null} |err: <nil>
2023/05/10 15:10:12 setNull |result:{"Val":null} |err: <nil>
2023/05/10 15:10:12 setValid |result:{"Val":22} |err: <nil>
3. 自定义类型
- 代码
package main
import (
"encoding/json"
"log"
"strconv"
)
type Int struct {
Exists bool // 表示是否存在
IsNull bool // 表示是否为null
Value int
}
// UnmarshalJSON 自定义反序列化方法
func (i *Int) UnmarshalJSON(data []byte) error {
// 如果调用了该方法,说明设置了该值
i.Exists = true
if string(data) == "null" {
// 表明该字段的值为 null
i.IsNull = true
return nil
} else {
i.IsNull = false
}
var temp int
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
i.Value = temp
return nil
}
// MarshalJSON 自定义序列化方法
func (i Int) MarshalJSON() ([]byte, error) {
if !i.Exists || i.IsNull {
return []byte("null"), nil // 注意:必须是小写null,不能是NULL、Null,原因Json不允许
}
return []byte(strconv.Itoa(i.Value)), nil
}
type Some struct{ Val Int }
func do(bytes []byte) (Some, error) {
var a struct{ Val Int }
err := json.Unmarshal(bytes, &a)
return a, err
}
func testDeserialize() {
notSet := []byte(`{}`)
setNull := []byte(`{"val": null}`)
setValid := []byte(`{"val": 123}`)
setWrongType := []byte(`{"val": "123"}`)
a, err := do(notSet)
log.Printf("NotSet |Exists:%t |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
a, err = do(setNull)
log.Printf("SetNull |Exists:%t |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
a, err = do(setValid)
log.Printf("SetValid |Exists:%t |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
a, err = do(setWrongType)
log.Printf("SetWrongType|Exists:%t |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
}
func testSerialize() {
notSet1 := Some{Int{Exists: false}}
notSet2 := Some{Int{Exists: false}}
setNull := Some{Int{Exists: true, IsNull: true}}
setValid := Some{Val: Int{Exists: true, IsNull: false, Value: 33}}
res, err := json.Marshal(notSet1)
log.Printf("NotSet1 |result:%s |err: %v\n", res, err)
res, err = json.Marshal(notSet2)
log.Printf("notSet2 |result:%s |err: %v\n", res, err)
res, err = json.Marshal(setNull)
log.Printf("setNull |result:%s |err: %v\n", res, err)
res, err = json.Marshal(setValid)
log.Printf("setValid |result:%s |err: %v\n", res, err)
}
func main() {
log.Println("deserialize test ...")
testDeserialize()
log.Println("serialize test ...")
testSerialize()
}
- 运行结果
% go run test_json.go
2023/05/10 15:15:50 deserialize test ...
2023/05/10 15:15:50 NotSet |Exists:false |IsNull:false |Value: 0 |err: <nil>
2023/05/10 15:15:50 SetNull |Exists:true |IsNull:true |Value: 0 |err: <nil>
2023/05/10 15:15:50 SetValid |Exists:true |IsNull:false |Value: 123 |err: <nil>
2023/05/10 15:15:50 SetWrongType|Exists:true |IsNull:false |Value: 0 |err: json: cannot unmarshal string into Go struct field .Val of type int
2023/05/10 15:15:50 serialize test ...
2023/05/10 15:15:50 NotSet1 |result:{"Val":null} |err: <nil>
2023/05/10 15:15:50 notSet2 |result:{"Val":null} |err: <nil>
2023/05/10 15:15:50 setNull |result:{"Val":null} |err: <nil>
2023/05/10 15:15:50 setValid |result:{"Val":33} |err: <nil>
4. 总结
在实际应用中,基本不用区分json的null
和未设置
,所以,都可以一并对应到struct的nil
,认为用户未设置
。
上述两种办法都是对Golang基本数据类型的补充,在使用过程中,会存在诸多不变,例如
- 使用之前,需要先判断是否为nil,或者是否exists。如果直接使用,可能直接panic
- 解决了json与struct的序列化问题,还需要关注如何兼容struct与数据库之间的数据读写
所以,在Golang官方没有改变的情况下,还是尽量使用基本数据类型,否则,操作起来比较麻烦。
另外,在对空数组、空字典的序列化,是符合预期的,能区分开nil和[]、{}。
- nil,对应json的null
- []int{}、make([]int, 0, 10),都对应json的[]
- map[string]any{}、make(map[string]any),都对应json的{}
json.Marshal() will return null for var myslice []int and [] for initialized slice myslice := []int{}