js/ts 程序员表示很惊讶,go 中居然要额外写代码解决 json 中的 null

最近学习 go 发现发现处理 json 中的 null 时,会这么难受,需要专门写一篇文章来讲解一下【🐶】

以下是正文

json 是一种常用的数据格式,在 go 使用 json 序列化和反序列化时比较方便的,但在使用过程中,会遇到一些问题,比如 null

由于 go 没有联合类型,当 json 中有个属性为 null 时,就无法直接将 null 转换成 nil 后赋值给某个具体的类型

比如下面这个例子:

  • Name 定一个的是 string 类型,但在 jsonname 的值为 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" {
    // ...
  }
}

注意事项

  1. ORM 框架会实现一个 NullString 的类型,
    • 当我们在定义 Model 时,如果某个字段可以为 NULL,则 ORM 框架会把它定义为 NullString 类型(下文讲解)
  2. 给指针赋值时,不能直接使用字面量,需要先定义一个变量,然后将变量的地址赋值给指针
  3. 使用指针时需要注意,这里会比较绕
    • 在判断是否为 nil 时,不需要使用 * 取值符
    • 在判断是否为 uccs 时,需要使用 * 取值符
  4. 当遇到 panic: runtime error: invalid memory address or nil pointer dereference 错误时,说明指针为 nil

也就是说使用指针时,我们最需要注意的是:在指针上取值时,一定要注意它是不是为 nil

自定义类型

我们使用结构体定义一个类型:NullString,它有两个属性 StringValid

  • String 用来存储字符串
  • Valid 用来标识 String 是否有值
    • 如果 Validtrue,则 String 有值
    • 如果 Validfalse,则 String 是空值 ""
go 复制代码
type NullString struct {
  String string
  Valid  bool
}

当我们定义好类型后,需要考考虑两个问题:

  • 如何解决 json 解析时 null 的问题
  • 如何向数据库进行读写

go 有个特点,你自定义的类型有某些方法,那么在某些场景下,这些方法会被调用

比如,序列化时,会调用 MarshalJSON 方法,反序列化时,会调用 UnmarshalJSON 方法

你的自定义类型实现了这两个方法,那么在序列化和反序列化时,这两个方法就会被调用

数据库读写是实现 ScanValue 方法

所以下面就从这两块讲起:

序列化和反序列化

我们给 NullString 类型添加两个方法 MarshalJSONUnmarshalJSON

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.Unmarshaljson 数据进行解析时
    • http.Request 读取 json Body
    • 使用 encoding/jsonDecoder 进行解码时
    • 对实现了 Unmarshaler 接口的对象调用 UnmarshalJSON 方法时
  • 反过来,将 struct 内容序列化为 json 时会触发 json.Marshal 的调用
    • 直接调用 json.Marshal 对一个对象进行编码
    • 使用 http.ResponseWriterWrite 方法响应 json 数据时
    • 使用 encoding/jsonEncoder 进行编码时
    • 对实现了 Marshaler 接口的对象调用 MarshalJSON 方法时

序列化和反序列化问题解决了,那如何向数据库进行读写呢?

数据库读写

我们再给 NullString 添加两个方法 ValueScan

  • 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 框架如 GORMdatabase/sql 等查询时,扫描结果到自定义模型
  • Valuer 接口的 Value 方法会在以下情况被调用
    • ORM 框架如 GORMdatabase/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 中其他类型也要实现这样的方法,比如 NullIntNullBool 等,可以参照这个 guregu/null 这个库

使用第三方库

  1. 第三方库 guregu/null 已经实现了上面的方法,我们可以直接使用
  2. ORM 一般都实现了这些功能
    • 需要注意的是有些 ORM 只实现了 ScannerValuer 接口,没有实现 MarshalJSONUnmarshalJSON 接口

总结

  1. 使用 string 只能满足必填的情况
  2. ORM 框架一般都实现了 ScannerValuer 接口,但是有些 ORM 没有实现 MarshalJSONUnmarshalJSON 接口,需要自己实现,或者使用第三方库
  3. 使用指针时,如 *string,需要注意指针是否为 nil
相关推荐
10km6 小时前
java:json-path支持fastjson作为JSON解析提供者的技术实现
java·json·fastjson·json-path
avilang9 小时前
如何在 Go 1.24+ 中管理 tool 依赖
go
程序员爱钓鱼10 小时前
用 Go 做浏览器自动化?chromedp 带你飞!
后端·go·trae
哈哈~haha14 小时前
ui5_Walkthrough_Step 7:JSON Model
json·mvc·module·ui5
随风一样自由16 小时前
React内逐行解释这个 package.json 文件,最近搞了个工厂AI生产平台,顺便来学习一下
学习·react.js·json·package
小信啊啊16 小时前
Go语言结构体
golang·go
wtsolutions17 小时前
Excel to JSON by WTSolutions 4.0.0 版本更新公告
json·excel·wps·插件·转换·加载项·wtsolutions
wtsolutions17 小时前
Excel to JSON by WTSolutions 4.0.0 Update Announcement
json·excel·wps·addin·wtsolutions·conversion
最笨的羊羊1 天前
Flink CDC系列之:Kafka 变更日志 JSON 格式工厂类 ChangeLogJsonFormatFactory
json·flink cdc系列·changelog·kafka 变更日志·json 格式工厂类·formatfactory
moxiaoran57531 天前
Go语言的常量
go