深入理解 Go 原生的 json 包

2024/01/19 Go 共 6264 字,约 18 分钟

JSON(JavaScript 对象表示法)是一种简单的数据交换格式。从语法上讲,它类似于 JavaScript 的对象和列表。它最常用于 Web 后端和浏览器中运行的 JavaScript 程序之间的通信,但它也用于许多其他地方。它的主页 json.org 提供了该标准的非常清晰和简洁的定义。 使用 json package 包,可以轻松地从 Go 程序中读取和写入 JSON 数据。

Encoding

为了对 JSON 数据进行编码,我们使用 Marshal 函数。

func Marshal(v interface{}) ([]byte, error)

给定 Go 数据结构 Message,

type Message struct {
    Name string
    Body string
    Time int64
}

和一个 Message 实例

m := Message{"Alice", "Hello", 1294706395881547000}

我们可以使用 json.Marshal 编组 m 的 JSON 编码版本:

b, err := json.Marshal(m)

如果一切顺利,err 将为 nil,b 将是包含以下 JSON 数据的 []byte:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

只有可以表示为有效 JSON 的数据结构才会被编码:

  • JSON 对象仅支持字符串作为键;要编码 Go 映射类型,它必须采用 map[string]T 的形式(其中 T 是 json 包支持的任何 Go 类型)。
  • Channel, 复数和函数类型无法编码。
  • 不支持循环数据结构;它们会导致 Marshal 进入无限循环。
  • 指针将被编码为它们指向的值(如果指针为 nil,则为“null”)。

json 包仅访问结构类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在 JSON 输出中。

举一个循环引用的例子

"Cyclic data structures" 指的是循环引用的数据结构,即某个数据结构包含对自身的引用。在 JSON 编码中,循环引用可能导致无限循环,因此 json.Marshal 会检测并避免处理这种情况。

以下是一个简单的例子,演示了一个循环引用的数据结构:

package main

import (
    "fmt"
    
    "encoding/json"
)

type Employee struct {
        Name     string
        Manager  *Employee // 对自身的引用
}

func main() {
        // 创建两个雇员实例,并建立循环引用
        alice := Employee{Name: "Alice"}
        bob := Employee{Name: "Bob", Manager: &alice}
        
        alice.Manager = &bob
        // 尝试编码包含循环引用的数据结构
        _, err := json.Marshal(alice)
        // 输出错误信息
        if err != nil {
                fmt.Println("Error:", err)
        } else {
                fmt.Println("No error. This won't be reached.")
        }
}

在这个例子中,Employee 结构体有一个 Manager 字段,该字段是对 Employee 自身的引用,形成了一个循环引用。当我们尝试使用 json.Marshal 编码包含循环引用的数据结构时,会产生一个错误。

Decoding

为了解码 JSON 数据,我们使用 Unmarshal 函数。

func Unmarshal(data []byte, v interface{}) error

我们必须首先创建一个存储解码数据的地方

var m Message

并调用 json.Unmarshal,向其传递 JSON 数据的 []byte 和指向 m 的指针

err := json.Unmarshal(b, &m)

如果 b 包含适合 m 的有效 JSON,则调用后 err 将为 nil,并且来自 b 的数据将存储在结构 m 中,就像通过如下赋值一样:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Unmarshal 如何识别存储解码数据的字段?对于给定的 JSON 键“Foo”,Unmarshal 将查找目标结构体的字段以查找(按优先顺序):

  • 带有“Foo”标签的导出字段(有关结构标签的更多信息,请参阅 Go 规范 https://go.dev/ref/spec#Struct_types,通过反射获取标签的值。),
  • 名为“Foo”的导出字段,或
  • 名为“FOO”或“FoO”的导出字段或“Foo”的其他不区分大小写的匹配项。

当 JSON 数据的结构与 Go 类型不完全匹配时会发生什么?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal 将仅解码它可以在目标类型中找到的字段。在这种情况下,仅填充 m 的 Name 字段,而 Food 字段将被忽略。当您希望从大型 JSON blob 中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构中任何未导出的字段都不会受到 Unmarshal 的影响。

举例

type Person struct {
   Name  string json:"name"
   AGe   int
   Money int
}

func main() {
   // JSON 数据
   jsonData := []byte({"name":"Alice","age":30,"food":"pizza"})

   // 创建一个 Person 实例
   var p Person

   // 反序列化 JSON 到 Person 实例
   err := json.Unmarshal(jsonData, &p)

   // 输出结果
   if err != nil {
      fmt.Println("Error:", err)
   } else {
      fmt.Printf("Name: %s\n", p.Name) // 输出 Name 字段
      fmt.Printf("Age: %d\n", p.AGe)   // 输出 Age 字段
      fmt.Printf("Sex: %d\n", p.Money)
   }
}

输出结果
Name: Alice
Age: 30
Sex: 0

但是如果事先不知道 JSON 数据的结构怎么办?

Generic JSON with interface

interface{}(空接口)类型描述了具有零个方法的接口。每个 Go 类型至少实现零个方法,因此满足空接口。

空接口作为通用容器类型:

var i interface{}
i = "a string"
i = 2011
i = 2.777

类型断言访问底层的具体类型:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

或者,如果基础类型未知,则类型开关确定类型:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i isn't one of the types above
}

json 包使用 map[string]interface{}和[]interface{}值来存储任意 JSON 对象和数组;它会很乐意将任何有效的 JSON blob 解组为简单的 interface{} 值。默认的具体 Go 类型是:

  • bool for JSON booleans,
  • float64 for JSON numbers,
  • string for JSON strings,
  • nil for JSON null.

Decoding arbitrary data 解码任意数据

考虑存储在变量 b 中的 JSON 数据:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

在不知道该数据结构的情况下,我们可以使用 Unmarshal 将其解码为 interface{} 值:

var f interface{}
err := json.Unmarshal(b, &f)

此时 f 中的 Go 值将是一个映射,其键是字符串,其值本身存储为空接口值:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

要访问此数据,我们可以使用类型断言来访问 f 的底层 map[string]interface{}:

m := f.(map[string]interface{})

然后,我们可以使用 range 语句迭代映射,并使用类型开关来访问其值作为具体类型:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

通过这种方式,您可以使用未知的 JSON 数据,同时仍然享受类型安全的好处。

Reference Types

让我们定义一个 Go 类型来包含上一个示例中的数据:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}
var m FamilyMember
err := json.Unmarshal(b, &m)

将数据解组为 FamilyMember 值可以按预期工作,但如果我们仔细观察,我们会发现发生了一件了不起的事情。使用 var 语句,我们分配了一个 FamilyMember 结构,然后向 Unmarshal 提供了指向该值的指针,但当时 Parent 字段是一个 nil 切片值。为了填充“Parents”字段,Unmarshal 在幕后分配了一个新切片。这是 Unmarshal 处理受支持的引用类型(指针、切片和映射)的典型方式。

这段描述涉及到 Go 中 JSON 反序列化时对引用类型的处理。在这里,我们定义了一个名为 FamilyMember 的结构体,其中包含一个切片字段 Parents

接下来,我们使用 json.Unmarshal 将 JSON 数据解码到 FamilyMember 类型的变量 m 中。在这个过程中,我们传递给 Unmarshal 的是变量 m 的指针(&m),这是因为 JSON 解码时需要知道解码后的数据应该存放在哪个变量中。

FamilyMember 结构体中,有一个切片字段 Parents。在 JSON 数据中,如果该字段是一个空数组(例如 []),那么在 Go 中对应的是一个 nil 切片。当我们传递 &m 这个指针给 Unmarshal 时,它会发现 Parents 字段是一个 nil 切片,并且会在解码的过程中为该字段分配一个新的切片。

这里是一个简单的例子:

package main

import (
        "encoding/json"
        "fmt"
)

type FamilyMember struct {
        Name    string
        Age     int
        Parents []string
}

func main() {
        // JSON 数据
        jsonData := []byte(`{"Name":"Alice","Age":30,"Parents":["John","Jane"]}`)

        // 创建一个 FamilyMember 实例
        var m FamilyMember

        // 反序列化 JSON 到 FamilyMember 实例
        err := json.Unmarshal(jsonData, &m)

        // 输出结果
        if err != nil {
                fmt.Println("Error:", err)
        } else {
                fmt.Printf("Name: %s\n", m.Name)
                fmt.Printf("Age: %d\n", m.Age)
                fmt.Printf("Parents: %v\n", m.Parents)
        }
}

在这个例子中,JSON 数据中的 Parents 字段是一个包含两个元素的数组。在 FamilyMember 结构体中,Parents 字段的初始值是 nil 切片。通过 json.Unmarshal,我们成功地将 JSON 数据解码到了 FamilyMember 实例中,并且 Parents 字段被填充为 ["John","Jane"]。这是因为在解码的过程中,Unmarshal 识别到 Parents 字段是一个 nil 切片,于是分配了一个新的切片并填充了数据。

考虑解组到此数据结构中:

type Foo struct {
    Bar *Bar
}

如果 JSON 对象中有 Bar 字段,Unmarshal 将分配一个新的 Bar 并填充它。如果不是,Bar 将保留为 nil 指针。

由此产生了一个有用的模式:如果您有一个接收几种不同消息类型的应用程序,您可以定义“接收者”结构,例如

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

发送方可以填充顶级 JSON 对象的 Cmd 字段和/或 Msg 字段,具体取决于他们想要通信的消息类型。当将 JSON 解码为 IncomingMessage 结构时,Unmarshal 将仅分配 JSON 数据中存在的数据结构。要知道要处理哪些消息,程序员只需测试 Cmd 或 Msg 是否不为零。

Streaming Encoders and Decoders

json 包提供了 Decoder 和 Encoder 类型来支持读取和写入 JSON 数据流的常见操作。 NewDecoder 和 NewEncoder 函数包装了 io.Reader 和 io.Writer 接口类型。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

下面是一个示例程序,它从标准输入读取一系列 JSON 对象,从每个对象中删除除 Name 字段之外的所有对象,然后将对象写入标准输出:

package main
import (
    "encoding/json"
    "log"
    "os"
)
func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

由于 Readers 和 Writers 无处不在,这些 Encoder 和 Decoder 类型可用于广泛的场景,例如读取和写入 HTTP 连接、WebSocket 或文件。

References

欲了解更多信息,请参阅 json package documentation. 有关 json 的示例用法,请参阅以下源文件 jsonrpc package. https://go.dev/blog/json

文档信息

Search

    Table of Contents