Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go处理json

浅析golang如何处理json中的null

作者:uccs

json 是一种常用的数据格式,在 go 使用 json 序列化和反序列化时比较方便的,但在使用过程中,会遇到一些问题,比如 null,所以下面我们就来看看golang如何处理json中的null吧

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

以下是正文

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

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

比如下面这个例子:

Name 定一个的是 string 类型,但在 jsonname 的值为 null,直接转换会报错

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 的指针类型,如下代码:

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,则使用 * 取值符取出值

// 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,它有两个属性 StringValid

String 用来存储字符串

Valid 用来标识 String 是否有值

type NullString struct {
  String string
  Valid  bool
}

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

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

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

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

数据库读写是实现 ScanValue 方法

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

序列化和反序列化

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

// 序列化时
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 的调用

反过来,将 struct 内容序列化为 json 时会触发 json.Marshal 的调用

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

数据库读写

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

// 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 的解构体改为:

type Tag struct {
  ID   int        `json:"id"`
  Name NullString `json:"name"`
}

不过这里要注意的一点是,在给 Name 赋值时,需要使用 NullString 进行赋值,如果下所示:

tag := Tag{
  ID:   1,
  Name: NullString{String: "hello", Valid: true},
}

最后需要注意的是,go 中其他类型也要实现这样的方法,比如 NullIntNullBool 等,可以参照这个 guregu/null 这个库

使用第三方库

第三方库 guregu/null 已经实现了上面的方法,我们可以直接使用

ORM 一般都实现了这些功能

需要注意的是有些 ORM 只实现了 ScannerValuer 接口,没有实现 MarshalJSONUnmarshalJSON 接口

总结

到此这篇关于浅析golang如何处理json中的null的文章就介绍到这了,更多相关go处理json内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文