浅析golang如何处理json中的null

 更新时间:2023年09月19日 11:17:56   作者: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 是否有值

  • 如果 Validtrue,则 String 有值
  • 如果 Validfalse,则 String 是空值 ""
type NullString struct {
  String string
  Valid  bool
}

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

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

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 的调用

  • 直接调用 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 方法会在从数据库读取时被调用
// 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 接口

总结

  • 使用 string 只能满足必填的情况
  • ORM 框架一般都实现了 ScannerValuer 接口,但是有些 ORM 没有实现 MarshalJSONUnmarshalJSON 接口,需要自己实现,或者使用第三方库
  • 使用指针时,如 *string,需要注意指针是否为 nil

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

相关文章

  • Go语言基础go doc命令用法及示例详解

    Go语言基础go doc命令用法及示例详解

    这篇文章主要为大家介绍了Go语言基础go doc命令的用法及示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步
    2021-11-11
  • Go语言操作MySQL的知识总结

    Go语言操作MySQL的知识总结

    Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。本文介绍了Go语言操作MySQL的相关知识,感兴趣的可以了解一下
    2022-11-11
  • Golang开发命令行之flag包的使用方法

    Golang开发命令行之flag包的使用方法

    这篇文章主要介绍Golang开发命令行及flag包的使用方法,日常命令行操作,相对应的众多命令行工具是提高生产力的必备工具,本文围绕该内容展开话题,需要的朋友可以参考一下
    2021-10-10
  • 详解Go 1.22 for循环的两处重要更新

    详解Go 1.22 for循环的两处重要更新

    这篇文章主要详细介绍了Go 1.22 for循环的两处重要更新,Go 1.22 版本于 2024 年 2 月 6 日发布,引入了几个重要的特性和改进,在语言层面上,这个版本对 for 循环进行了两处更新,本文将会对 for 循环的两个更新进行介绍,需要的朋友可以参考下
    2024-02-02
  • Go语言面试题之select和channel的用法

    Go语言面试题之select和channel的用法

    金九银十面试季到了(PS:貌似今年一年都是面试季),就业环境很差,导致从业人员不得不卷。本文将重点讲解一下Go面试进阶知识点之select和channel,需要的可以参考一下
    2022-09-09
  • Golang 使用map需要注意的几个点

    Golang 使用map需要注意的几个点

    这篇文章主要介绍了Golang 使用map需要注意的几个点,帮助大家更好的理解和学习golang,感兴趣的朋友可以了解下
    2020-09-09
  • golang创建文件目录os.Mkdir,os.MkdirAll的区别说明

    golang创建文件目录os.Mkdir,os.MkdirAll的区别说明

    本文主要讲述os.Mkdir、os.MkdirAll区别以及在创建文件目录过程中的一些其他技巧,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • go get 和 go install 对比介绍

    go get 和 go install 对比介绍

    go install和go get都是Go语言的工具命令,但它们之间有一些区别。go get:用于从远程代码存储库(如 GitHub)中下载或更新Go代码包。go install:用于编译并安装 Go 代码包,本文go get和go install对比介绍的非常详细,需要的朋友可以参考一下
    2023-04-04
  • Go 自定义package包设置与导入操作

    Go 自定义package包设置与导入操作

    这篇文章主要介绍了Go 自定义package包设置与导入操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 一文带你深入理解Go语言中的sync.Cond

    一文带你深入理解Go语言中的sync.Cond

    sync.Cond 表示的是条件变量,它是一种同步机制,用来协调多个 goroutine 之间的同步。本文将通过示例为大家介绍Go语言中sync.Cond的使用,需要的可以参考一下
    2023-01-01

最新评论