最近学习 go
发现发现处理 json
中的 null
时,会这么难受,需要专门写一篇文章来讲解一下【🐶】
以下是正文
json
是一种常用的数据格式,在 go
使用 json
序列化和反序列化时比较方便的,但在使用过程中,会遇到一些问题,比如 null
由于 go
没有联合类型,当 json
中有个属性为 null
时,就无法直接将 null
转换成 nil
后赋值给某个具体的类型
比如下面这个例子:
Name
定一个的是string
类型,但在json
中name
的值为null
,直接转换会报错
go
type Tag struct {
ID int `json:"id"`
Name string `json:"name"`
}
tag := Tag{
ID: 1,
Name: nil, // 这里会报错
}
这种问题不光出现在 json
解析时,还会出现在数据库读写时
比如在数据库中,某个字段的值为 NULL
,在读取时,会被解析成 nil
,但是 go
中的类型是不能直接赋值为 nil
的
所以在这两种场景下该怎么解决呢?
一般有三种方法:
- 使用指针
- 自定义类型
- 使用第三方库
使用指针
go
的指针类型是可以赋值为 nil
的,所以我们使用指针解决这个问题
我们把上面例子中的 Name
定义为 string
的指针类型,如下代码:
go
type Tag struct {
ID int `json:"id"`
Name *string `json:"name"`
}
name := "uccs" // 定义一个 string 类型的变量,因为不能把一个字面量直接赋值给指针类型
tag := Tag{
ID: 1,
Name: &name, // 将 name 的地址赋值给 Name,使用 & 地址符
}
在使用时,需要先判断一下 Name
是为 nil
,如果不为 nil
,则使用 *
取值符取出值
go
// Name 是指针类型,判断是否为 nil 时不需要使用 * 取值符
if tag.Name != nil {
// Name 是指针类型,取值时需要使用 * 取值符
if *tag.Name == "uccs" {
// ...
}
}
注意事项
ORM
框架会实现一个NullString
的类型,- 当我们在定义
Model
时,如果某个字段可以为NULL
,则ORM
框架会把它定义为NullString
类型(下文讲解)
- 当我们在定义
- 给指针赋值时,不能直接使用字面量,需要先定义一个变量,然后将变量的地址赋值给指针
- 使用指针时需要注意,这里会比较绕
- 在判断是否为
nil
时,不需要使用*
取值符 - 在判断是否为
uccs
时,需要使用*
取值符
- 在判断是否为
- 当遇到
panic: runtime error: invalid memory address or nil pointer dereference
错误时,说明指针为nil
也就是说使用指针时,我们最需要注意的是:在指针上取值时,一定要注意它是不是为 nil
自定义类型
我们使用结构体定义一个类型:NullString
,它有两个属性 String
和 Valid
String
用来存储字符串Valid
用来标识String
是否有值- 如果
Valid
为true
,则String
有值 - 如果
Valid
为false
,则String
是空值""
- 如果
go
type NullString struct {
String string
Valid bool
}
当我们定义好类型后,需要考考虑两个问题:
- 如何解决
json
解析时null
的问题 - 如何向数据库进行读写
go
有个特点,你自定义的类型有某些方法,那么在某些场景下,这些方法会被调用
比如,序列化时,会调用 MarshalJSON
方法,反序列化时,会调用 UnmarshalJSON
方法
你的自定义类型实现了这两个方法,那么在序列化和反序列化时,这两个方法就会被调用
数据库读写是实现 Scan
和 Value
方法
所以下面就从这两块讲起:
序列化和反序列化
我们给 NullString
类型添加两个方法 MarshalJSON
和 UnmarshalJSON
go
// 序列化时
func (ns NullString) MarshalJSON() ([]byte, error) {
// 如果 Valid 为 true,则返回 String 的 json 序列化结果
if ns.Valid {
return []byte(`"` + ns.String + `"`), nil
}
// 如果 Valid 为 false,则返回 null 序列化的结果
return []byte("null"), nil
}
// 反序列化
func (ns *NullString) UnmarshalJSON(data []byte) error {
// 如果 data 为 null,则 Valid 为 false
// String 为空字符串
if string(data) == "null" {
ns.String, ns.Valid = "", false
return nil
}
// 否则,将 data 反序列化到 String 中
// 并将 Valid 设置为 true
if err := json.Unmarshal(data, &ns.String); err != nil {
return err
}
ns.Valid = true
return nil
}
有了这两个方法之后,我们就解决了 json
解析时 null
的问题
是什么时候会触发这两个方法呢?
- 从
json
内容解析填充struct
的场景时会触发UnmarshalJSON
的调用- 直接调用
json.Unmarshal
对json
数据进行解析时 http.Request
读取json Body
时- 使用
encoding/json
的Decoder
进行解码时 - 对实现了
Unmarshaler
接口的对象调用UnmarshalJSON
方法时
- 直接调用
- 反过来,将
struct
内容序列化为json
时会触发json.Marshal
的调用- 直接调用
json.Marshal
对一个对象进行编码 - 使用
http.ResponseWriter
的Write
方法响应json
数据时 - 使用
encoding/json
的Encoder
进行编码时 - 对实现了
Marshaler
接口的对象调用MarshalJSON
方法时
- 直接调用
序列化和反序列化问题解决了,那如何向数据库进行读写呢?
数据库读写
我们再给 NullString
添加两个方法 Value
和 Scan
Value
方法会在写入数据库时被调用Scan
方法会在从数据库读取时被调用
go
// Scan 方法在 数据库读取时被调用
func (ns *NullString) Scan(value interface{}) error {
// 如果 value 为 nil,则 Valid 为 false,String 为空字符串
if value == nil {
ns.String, ns.Valid = "", false
return nil
}
// 否则,将 value 断言为 string 类型,断言成功 Valid 为 true,String 为 value
ns.String, ns.Valid = value.(string)
return nil
}
// Value 方法 在写入数据库时被调用
func (ns NullString) Value() (driver.Value, error) {
// 如果 Valid 为 false,则返回 nil
if !ns.Valid {
return nil, nil
}
// 否则,返回 String
return ns.String, nil
}
添加这两个方法后,我们就可以向数据库中写入 null
了
是什么时候会触发这两个方法呢?
Scanner
接口的Scan
方法会在以下情况被调用ORM
框架如GORM
、database/sql
等查询时,扫描结果到自定义模型
Valuer
接口的Value
方法会在以下情况被调用ORM
框架如GORM
、database/sql
构造写入语句时,获取自定义模型的值
使用
将上面 Tag
的解构体改为:
go
type Tag struct {
ID int `json:"id"`
Name NullString `json:"name"`
}
不过这里要注意的一点是,在给 Name
赋值时,需要使用 NullString
进行赋值,如果下所示:
go
tag := Tag{
ID: 1,
Name: NullString{String: "hello", Valid: true},
}
最后需要注意的是,go
中其他类型也要实现这样的方法,比如 NullInt
,NullBool
等,可以参照这个 guregu/null 这个库
使用第三方库
- 第三方库 guregu/null 已经实现了上面的方法,我们可以直接使用
ORM
一般都实现了这些功能- 需要注意的是有些
ORM
只实现了Scanner
和Valuer
接口,没有实现MarshalJSON
和UnmarshalJSON
接口
- 需要注意的是有些
总结
- 使用
string
只能满足必填的情况 ORM
框架一般都实现了Scanner
和Valuer
接口,但是有些ORM
没有实现MarshalJSON
和UnmarshalJSON
接口,需要自己实现,或者使用第三方库- 使用指针时,如
*string
,需要注意指针是否为nil