如何在Go中使用JSON

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。

介绍

在现代程序中,一个程序和另一个程序之间的通信很重要。 无论是 Go 程序检查用户是否可以访问另一个程序,JavaScript 程序获取过去订单列表以显示在网站上,还是 Rust[X183X ] 程序从文件中读取测试结果,程序需要一种方法来为其他程序提供数据。 然而,许多编程语言都有自己的内部存储数据的方式,这是其他语言无法理解的。 为了允许这些语言进行交互,需要将数据转换为他们都能理解的通用格式。 其中一种格式 JSON 是通过 Internet 以及在同一系统中的程序之间传输数据的流行方式。

许多现代编程语言在其标准库中包含一种将数据与 JSON 相互转换的方法,Go 也是如此。 通过使用 Go 提供的 encoding/json 包,您的 Go 程序还可以与任何其他可以使用 JSON 进行通信的系统进行交互。

在本教程中,您将首先创建一个程序,该程序使用 encoding/json 包将 map 中的数据编码为 JSON 数据,然后更新您的程序以使用 struct 类型而是对数据进行编码。 之后,您将更新程序以将 JSON 数据解码为 map,然后最终将 JSON 数据解码为 struct 类型。

先决条件

要遵循本教程,您将需要:

  • Go version 1.16 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
  • 熟悉 JSON,可以在 An Introduction to JSON 中找到。
  • 使用 Go 结构标签自定义 struct 类型字段的能力。 更多信息可以在 How To Use Struct Tags in Go 中找到。
  • (可选)了解如何在 Go 中创建日期和时间值。 您可以在 如何在 Go 中使用日期和时间中阅读更多内容。

使用地图生成 JSON

Go 对 JSON 编码和解码的支持由标准库的 encoding/json 包提供。 您将从该包中使用的第一个函数是 json.Marshal 函数。 Marshalling,有时也称为序列化,是将内存中的程序数据转换为可以传输或保存在其他地方的格式的过程。 json.Marshal 函数用于将 Go 数据转换为 JSON 数据。 json.Marshal 函数接受 interface{} 类型作为编组为 JSON 的值,因此任何值都可以作为参数传入,并将返回 JSON 数据作为结果。 在本节中,您将创建一个程序,使用 json.Marshal 函数从 Go map 值生成包含各种类型数据的 JSON,然后将这些值打印到输出。

大多数 JSON 表示为一个对象,具有 string 键和各种其他类型作为值。 因此,在 Go 中生成 JSON 数据最灵活的方法是使用 string 键和 interface{} 值将数据放入 mapstring键可以直接翻译成JSON对象键,interface{}值允许该值是任何其他值,无论是stringint,甚至另一个 map[string]interface{}

要开始在程序中使用 encoding/json 包,您需要有该程序的目录。 在本教程中,您将使用一个名为 projects 的目录。

首先,创建 projects 目录并导航到它:

mkdir projects
cd projects

接下来,为您的项目创建目录。 在这种情况下,使用目录 jsondata

mkdir jsondata
cd jsondata

jsondata 目录中使用 nano 或您喜欢的编辑器打开 main.go 文件:

nano main.go

main.go 文件中,您将添加一个 main 函数来运行您的程序。 接下来,您将添加具有各种键和数据类型的 map[string]interface{} 值。 然后,您将使用 json.Marshal 函数将 map 数据编组为 JSON 数据。

将以下行添加到 main.go

main.go

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
    }

    jsonData, err := json.Marshal(data)
    if err != nil {
        fmt.Printf("could not marshal json: %s\n", err)
        return
    }

    fmt.Printf("json data: %s\n", jsonData)
}

您将在 data 变量中看到每个值都有一个 string 作为键,但这些键的值各不相同。 一个是 int 值,另一个是 bool 值,甚至是另一个 map[string]interface{} 值,其中包含 []int 值。

当您将 data 变量传递给 json.Marshal 时,该函数将查看您提供的所有值并确定它们的类型以及如何在 JSON 中表示它们。 如果翻译中有任何问题,json.Marshal 函数将返回描述问题的 error。 但是,如果转换成功,jsonData 变量将包含编组 JSON 数据的 []byte。 由于可以使用 myString := string(jsonData) 或格式字符串中的 %s verb[]byte 值转换为 string 值,因此您然后可以使用 fmt.Printf 将 JSON 数据打印到屏幕上。

保存并关闭文件。

要查看程序的输出,请使用 go run 命令并提供 main.go 文件:

go run main.go

您的输出将类似于以下内容:

Outputjson data: {"boolValue":true,"intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

在输出中,您将看到顶级 JSON 值是一个由围绕它的花括号 ({}) 表示的对象。 您在 data 中包含的所有值都存在。 您还将看到 objectValuemap[string]interface{} 被转换为另一个由 {} 包围的 JSON 对象,并且其中还包含 arrayValue 和数组值[1,2,3,4]

JSON 中的编码时间

但是,encoding/json 包不仅支持 stringint 值等类型。 它还可以编码更复杂的类型。 它支持的更复杂的类型之一是 time 包中的 time.Time 类型。

注意: 关于 Go 的 time 包的更多信息,请查看教程,如何在 Go 中使用日期和时间。


要查看实际情况,请再次打开 main.go 文件并使用 time.Date 函数将 time.Time 值添加到数据中:

main.go

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
        "dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
    }

    ...
}

此更新会将 UTC 时区中的日期 March 2, 2022, 和时间 9:10:00 AM 分配给 dateValue 键。

保存更改后,使用与以前相同的 go run 命令再次运行程序:

go run main.go

您的输出将类似于以下内容:

Outputjson data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

这次在输出中,您将在 JSON 数据中看到 dateValue 字段,其时间使用 RFC 3339 格式进行格式化,这是一种用于将日期和时间表示为 string 值。

在 JSON 中编码 null

根据您的程序与之交互的系统,您可能需要在 JSON 数据中发送 null 值,Go 的 encoding/json 包也可以为您处理。 使用 map,只需添加具有 nil 值的新 string 键。

要将几个 null 值添加到 JSON 输出,请再次打开 main.go 文件并添加以下行:

main.go

...

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
                "dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
        "nullStringValue": nil,
        "nullIntValue":    nil,
    }

    ...
}

您添加到数据中的值具有表示它是 string 值或 int 值的键,但实际上代码中没有任何内容可以使其成为其中任何一个值。 由于 map 具有 interface{} 值,所有代码都知道 interface{} 的值是 nil。 由于您仅使用此 map 将 Go 数据转换为 JSON 数据,因此此时的区别并没有什么区别。

将更改保存到 main.go 后,使用 go run 运行程序:

go run main.go

您的输出将类似于以下内容:

Outputjson data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"nullIntValue":null,"nullStringValue":null,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

现在在输出中,您将看到 nullIntValuenullStringValue 字段包含在 JSON null 值中。 这样,您仍然可以使用 map[string]interface{} 值将 Go 数据转换为具有预期字段的 JSON 数据。

在本节中,您创建了一个可以将 map[string]interface{} 值编组为 JSON 数据的程序。 然后,您在数据中添加了一个 time.Time 字段,还包括了一对 null 值字段。

虽然使用 map[string]interface{} 来编组 JSON 数据可能非常灵活,但如果您需要在多个地方发送相同的数据,它也会变得很麻烦。 如果将此数据复制到代码中的多个位置,则很容易意外键入字段名称,或将不正确的数据分配给字段。 在这种情况下,使用 struct 类型来表示您要转换为 JSON 的数据会很有好处。

使用结构生成 JSON

使用像 Go 这样的 静态类型 语言的好处之一是,您可以使用这些类型让编译器检查或强制执行程序的一致性。 Go 的 encoding/json 包允许您通过定义 struct 类型来表示 JSON 数据来利用这一点。 您可以使用 struct tags 控制如何转换 struct 中包含的数据。 在本节中,您将更新程序以使用 struct 类型而不是 map 类型来生成 JSON 数据。

当您使用 struct 定义 JSON 数据时,您希望翻译的字段名称(不是 struct 类型名称本身)必须导出,这意味着它们必须以大写字母开头,例如作为 IntValueencoding/json 包将无法访问字段以将其转换为 JSON。 如果不使用结构标签来控制这些字段的命名,则字段名称将直接翻译为它们在 struct 上的样子。 使用默认名称可能是您希望在 JSON 数据中使用的名称,具体取决于您希望数据的形成方式。 如果是这种情况,您不需要添加任何结构标签。 但是,许多 JSON 使用者使用 intValueint_value 等名称格式作为其字段名称,因此添加这些结构标签将允许您控制转换的发生方式。

例如,假设您有一个 struct 和一个名为 IntValue 的字段,您将其编组为 JSON:

type myInt struct {
    IntValue int
}

data := &myInt{IntValue: 1234}

如果您使用 json.Marshal 函数将 data 变量编组为 JSON,您最终会得到以下值:

{"IntValue":1234}

但是,如果您的 JSON 使用者希望该字段被命名为 intValue 而不是 IntValue,那么您需要一种方法来告诉 encoding/json。 由于 json.Marshal 不知道您希望该字段在 JSON 数据中命名的内容,因此您可以通过向该字段添加一个结构标记来告诉它。 通过将 json 结构标记添加到值为 intValueIntValue 字段,您告诉 json.Marshal 它应该使用名称 intValue生成 JSON 数据时:

type myInt struct {
    IntValue int `json:"intValue"`
}

data := &myInt{IntValue: 1234}

这一次,如果你将 data 变量编组为 JSON,json.Marshal 函数将看到 json 结构标记并且知道将字段命名为 intValue,所以你'会得到你预期的结果:

{"intValue":1234}

现在,您将更新您的程序以对 JSON 数据使用 struct 值。 您将添加一个 myJSON struct 类型来定义您的顶级 JSON 对象,以及一个 myObject struct 来定义您的内部 JSON 对象ObjectValue 字段。 您还将向每个字段添加一个 json 结构标记,以告诉 json.Marshal 如何在 JSON 数据中命名它们。 您还需要更新 data 变量分配以使用 myJSON 结构,声明它类似于您对任何其他 Go struct 的声明。

打开您的 main.go 文件并进行以下更改:

main.go

...

type myJSON struct {
    IntValue        int       `json:"intValue"`
    BoolValue       bool      `json:"boolValue"`
    StringValue     string    `json:"stringValue"`
    DateValue       time.Time `json:"dateValue"`
    ObjectValue     *myObject `json:"objectValue"`
    NullStringValue *string   `json:"nullStringValue"`
    NullIntValue    *int      `json:"nullIntValue"`
}

type myObject struct {
    ArrayValue []int `json:"arrayValue"`
}

func main() {
    otherInt := 4321
    data := &myJSON{
        IntValue:    1234,
        BoolValue:   true,
        StringValue: "hello!",
        DateValue:   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        ObjectValue: &myObject{
            ArrayValue: []int{1, 2, 3, 4},
        },
        NullStringValue: nil,
        NullIntValue:    &otherInt,
    }

    ...
}

其中许多更改与之前的 IntValue 字段名称示例类似,但其中一些更改值得特别指出。 其中之一,ObjectValue 字段,使用 *myObject 的引用类型来告诉 JSON 编组器期望对 myObject 值或 [ 的引用X160X] 值。 这就是您可以定义一个深度为多层自定义对象的 JSON 对象的方式。 如果您的 JSON 数据需要它,您还可以在 myObject 类型中引用另一个 struct 类型,依此类推。 使用这种模式,您可以使用 Go struct 类型描述非常复杂的 JSON 对象。

上述代码中要查看的另一对字段是 NullStringValueNullIntValue。 与 StringValueIntValue 不同,这些值的类型是引用类型 *string*int。 默认情况下,stringint 类型不能有 nil 的值,因为它们的“空”值是 ""0。 因此,如果要表示可以是一种类型或 nil 的字段,则需要将其设为引用。 例如,假设您有一份用户问卷,并且您希望能够表示用户是否选择不回答问题(null 值),或者用户没有问题的答案( "" 值)。

此代码还更新了 NullIntValue 字段,将 4321 值分配给它,以显示如何将值分配给引用类型,例如 *int。 在 Go 中,您只能使用变量创建对 原始类型 的引用,例如 intstring。 因此,为了给 NullIntValue 字段赋值,首先将值赋值给另一个变量 otherInt,然后使用 &otherInt 获取对它的引用(而不是直接做&4321)。

保存更新后,使用 go run 运行程序:

go run main.go

您的输出将类似于以下内容:

Outputjson data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullStringValue":null,"nullIntValue":4321}

您将看到此输出与使用 map[string]interface{} 值时相同,除了这次 nullIntValue 的值为 4321 因为那是 [ X155X]。

最初,设置 struct 值可能需要一些额外的时间,但是一旦定义了它们,就可以在代码中反复使用它们,无论在哪里使用,结果都是一样的他们。 您也可以在一个地方更新它们,而不是试图找到每个可以使用 map 的地方。

Go 的 JSON 编组器还允许您根据值是否为空来控制是否应将字段包含在 JSON 输出中。 有时您可能有一个大型 JSON 对象或您不想一直包含的可选字段,因此省略这些字段可能很有用。 通过 json 结构标签中的 omitempty 选项控制字段是否为空时是否省略。

现在,更新您的程序以使 NullStringValue 字段 omitempty 并添加一个名为 EmptyString 的新字段,具有相同的选项:

main.go

...

type myJSON struct {
    ...
    
    NullStringValue *string   `json:"nullStringValue,omitempty"`
    NullIntValue    *int      `json:"nullIntValue"`
    EmptyString     string    `json:"emptyString,omitempty"`
}

...

现在,当 myJSON 被编组时,如果 EmptyStringNullStringValue 字段的值为空,则它们都将从输出中排除。

保存更改后,使用 go run 运行程序:

go run main.go

您的输出将类似于以下内容:

Outputjson data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullIntValue":4321}

这次在输出中,您将看到 nullStringValue 字段不再出现。 由于具有 nil 值将其视为空,因此 omitempty 选项将其从输出中排除。 您还会看到新的 emptyString 字段也不包括在内。 即使 emptyString 值不是 nil,字符串的默认 "" 值也被认为是空的,因此它也被排除在外。

在本节中,您更新了您的程序以使用 struct 类型来生成具有 json.Marshal 而不是 map 类型的 JSON 数据。 您还更新了程序以从 JSON 输出中省略空字段。

但是,为了让您的程序很好地适应 JSON 生态系统,您需要做的不仅仅是生成 JSON 数据。 您还需要能够读取响应您的请求而发送的 JSON 数据,或其他向您发送请求的系统。 encoding/json 包还提供了一种将 JSON 数据解码为各种 Go 类型的方法。 在下一部分中,您将更新程序以将 JSON 字符串解码为 Go map 类型。

使用地图解析 JSON

与本教程的第一部分类似,您使用 map[string]interface{} 作为一种灵活的方式来生成 JSON 数据,您也可以使用它作为一种灵活的方式来读取 JSON 数据。 json.Unmarshal 函数,本质上与 json.Marshal 函数相反,它将获取 JSON 数据并将其转换回 Go 数据。 您向 json.Unmarshal 提供 JSON 数据以及 Go 变量以将未编组的数据放入其中,如果无法执行此操作,它将返回 error 值,或者返回 [ X181X] 成功时的错误值。 在本节中,您将更新您的程序以使用 json.Unmarshal 函数将 JSON 数据从预定义的 string 值读取到 map 变量中。 您还将更新程序以将 Go 数据打印到输出。

现在,更新您的程序以使用 json.Unmarshal 将 JSON 数据解组为 map[string]interface{}。 您将首先将原始 data 变量替换为包含 JSON 字符串的 jsonData 变量。 然后,您将声明一个新的 data 变量作为 map[string]interface{} 以接收 JSON 数据。 最后,您将使用 json.Unmarshal 和这些变量来访问 JSON 数据。

打开 main.go 文件并将 main 函数中的行替换为以下内容:

main.go

...

func main() {
    jsonData := `
        {
            "intValue":1234,
            "boolValue":true,
            "stringValue":"hello!",
            "dateValue":"2022-03-02T09:10:00Z",
            "objectValue":{
                "arrayValue":[1,2,3,4]
            },
            "nullStringValue":null,
            "nullIntValue":null
        }
    `

    var data map[string]interface{}
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Printf("could not unmarshal json: %s\n", err)
        return
    }

    fmt.Printf("json map: %v\n", data)
}

在此更新中,jsonData 变量是使用 原始字符串文字 设置的,以允许声明跨越多行以便于阅读。 将 data 声明为 map[string]interface{} 后,将 jsonDatadata 传递给 json.Unmarshal 以将 JSON 数据解组到 [ X128X] 变量。

jsonData 变量作为 []byte 传递给 json.Unmarshal 因为该函数需要 []byte 类型并且 jsonData 最初定义为string 类型。 这是因为 Go 中的 string 可以转换为 []byte,反之亦然。 data 变量作为引用传递,因为为了使 json.Unmarshal 将数据放入变量中,它需要引用变量在内存中的存储位置。

最后,一旦 JSON 数据被解组到 data 变量中,就可以使用 fmt.Printf 将其打印到屏幕上。

要运行更新后的程序,请保存更改并使用 go run 运行程序:

go run main.go

输出将类似于以下内容:

Outputjson map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]

这一次,您的输出显示了 JSON 翻译的 Go 端。 您有一个 map 值,其中包含来自 JSON 数据的各种字段。 您会看到,即使是 JSON 数据中的 null 字段也会显示在地图中。

现在,由于您的 Go 数据位于 map[string]interface{} 中,因此需要进行一些工作来使用这些数据。 您需要使用所需的 string 键值从 map 获取值,然后您需要确保收到的值是您期望的值,因为它作为 [ X199X] 值。

为此,请打开 main.go 文件并更新您的程序以使用以下代码读取 dateValue 字段:

main.go

...

func main() {
    ...
    
    fmt.Printf("json map: %v\n", data)

    rawDateValue, ok := data["dateValue"]
    if !ok {
        fmt.Printf("dateValue does not exist\n")
        return
    }
    dateValue, ok := rawDateValue.(string)
    if !ok {
        fmt.Printf("dateValue is not a string\n")
        return
    }
    fmt.Printf("date value: %s\n", dateValue)
}

在此更新中,您使用 data["dateValue"]rawDateValue 作为 interface{} 类型,并使用 ok 变量确保 [X129X ] 字段位于 map 中。

然后,您使用 类型断言 断言 rawDateValue 的类型实际上是 string 值,并将其分配给变量 dateValue。 之后,您再次使用 ok 变量来确保断言成功。

最后,您使用 fmt.Printf 打印 dateValue

要再次运行更新后的程序,请保存更改并使用 go run 运行它:

go run main.go

您的输出将类似于以下内容:

Outputjson map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]
date value: 2022-03-02T09:10:00Z

您可以看到 date value 行显示从 map 提取并转换为 string 值的 dateValue 字段。

在本节中,您更新了程序以使用带有 map[string]interface{} 变量的 json.Unmarshal 函数将 JSON 数据解组为 Go 数据。 然后,您更新了程序以从围棋数据中提取 dateValue 的值并将其打印到屏幕上。

但是,此更新确实显示了使用 map[string]interface{} 在 Go 中解组 JSON 的缺点之一。 由于 Go 不知道每个字段是哪种类型的数据(它唯一知道的是它是一个 interface{}),它可以做的最好的解组数据就是做出最好的猜测。 这意味着无法为您解组 dateValue 字段的 time.Time 等复杂值,只能作为 string 访问。 如果您尝试以这种方式访问 map 中的任何数值,则会发生类似的问题。 由于 json.Unmarshal 不知道该数字是否应该是 intfloatint64 等等,所以它可以做出的最佳猜测是将其放入可用的最灵活的数字类型中,即 float64

虽然使用 map 来解码 JSON 数据可能很灵活,但在解释您拥有的数据时也会为您留下更多的工作。 类似于 json.Marshal 函数可以使用 struct 值来生成 JSON 数据,json.Unmarshal 函数可以使用 struct 值来读取 JSON 数据。 这可以帮助消除使用 map 的类型断言复杂性,方法是使用 struct 字段上的类型定义来确定 JSON 数据应解释为哪些类型。 在下一节中,您将更新您的程序以使用 struct 类型来消除这些复杂性。

使用结构解析 JSON

当您读取 JSON 数据时,您很有可能已经知道所接收数据的结构; 否则,将难以解释。 您可以使用这些结构知识为 Go 提供一些关于您的数据是什么样的以及您期望的数据类型的提示。

在上一节中,您定义了 myJSONmyObject struct 值并添加了 json 结构标签,让 Go 在生成 JSON 时知道如何命名字段. 现在您可以使用相同的 struct 值来解码您一直在使用的 JSON 字符串,如果您正在编组和解组相同的 JSON 数据,这有助于减少程序中的重复代码。 使用 struct 解组 JSON 数据的另一个好处是,您可以告诉 Go 每个字段预期的数据类型。 最后,您还可以使用 Go 的编译器检查您在字段上使用的名称是否正确,而不是在您将与 map 值一起使用的 string 值中可能会丢失拼写错误.

现在,打开您的 main.go 文件并更新 data 变量声明以使用对 myJSON struct 的引用,并添加一些 [X141X ] 行显示 myJSON 上各个字段的数据:

main.go

...

func main() {
    ...
    
    var data *myJSON
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Printf("could not unmarshal json: %s\n", err)
        return
    }

    fmt.Printf("json struct: %#v\n", data)
    fmt.Printf("dateValue: %#v\n", data.DateValue)
    fmt.Printf("objectValue: %#v\n", data.ObjectValue)
}

由于您之前定义了 struct 类型,因此您只需更新 data 字段的类型以支持解组为 struct。 其余的更新显示了 struct 本身的一些数据。

现在,保存更新并使用 go run 运行程序:

go run main.go

您的输出将类似于以下内容:

Outputjson struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x1400011c180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

这次在输出中有几件事需要注意。 您将在 json struct 行和 dateValue 行中看到 JSON 数据中的日期值现已转换为 time.Time 值([X155X ] 格式是 %#v 用作格式动词时显示的内容)。 由于 Go 能够在 myJSONDateValue 字段上看到 time.Time 类型,因此它也能够为您解析 string 值。

需要注意的另一件事是 EmptyString 显示在 json struct 行上,即使它没有包含在原始 JSON 数据中。 如果某个字段包含在用于 JSON 解组的 struct 中,并且未包含在被解组的 JSON 数据中,则该字段仅设置为其类型的默认值并被忽略。 通过这种方式,您可以安全地定义 JSON 数据可能具有的所有可能字段,而不必担心如果流程的任一侧都不存在字段时会出现错误。 NullStringValueNullIntValue 也都设置为默认值 nil,因为 JSON 数据表明它们的值为 null,但它们也将设置为nil 如果这些字段已从 JSON 数据中排除。

类似于当 JSON 数据中缺少 emptyString 字段时 json.Unmarshal 忽略 struct 上的 EmptyString 字段,反之亦然。 如果一个字段包含在 JSON 数据中,但在 Go struct 上没有相应的字段,则该 JSON 字段将被忽略并继续解析下一个 JSON 字段。 这样,如果您正在读取的 JSON 数据非常大,而您的程序只关心其中的一小部分字段,您可以选择创建一个仅包含您关心的字段的 struct。 JSON 数据中包含的任何未在 struct 上定义的字段都将被忽略,Go 的 JSON 解析器将继续处理下一个字段。

要查看实际情况,请最后一次打开 main.go 文件并更新 jsonData 以包含 myJSON 中未包含的字段:

main.go

...

func main() {
    jsonData := `
        {
            "intValue":1234,
            "boolValue":true,
            "stringValue":"hello!",
            "dateValue":"2022-03-02T09:10:00Z",
            "objectValue":{
                "arrayValue":[1,2,3,4]
            },
            "nullStringValue":null,
            "nullIntValue":null,
            "extraValue":4321
        }
    `

    ...
}

添加 JSON 数据后,保存文件并使用 go run 运行它:

go run main.go

您的输出将类似于以下内容:

Outputjson struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x14000126180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

您不应该看到此输出与之前的输出有任何区别,因为 Go 将忽略 JSON 数据中的 extraValue 字段并继续。

在本节中,您更新了程序以使用您之前定义的 struct 类型来解组 JSON 数据。 您看到了 Go 如何为您解析 time.Time 值并忽略在 struct 类型上定义的 EmptyString 字段,但不在 JSON 数据中。 您还向 JSON 数据添加了一个附加字段,以查看 Go 将安全地继续解析数据,即使您仅在 JSON 数据中定义了字段的子集。

结论

在本教程中,您创建了一个新程序来使用 Go 标准库中的 encoding/json 包。 首先,您使用具有 map[string]interface{} 类型的 json.Marshal 函数以灵活的方式创建 JSON 数据。 然后,您更新了您的程序以使用带有 json 结构标记的 struct 类型,以使用 json.Marshal 以一致且可靠的方式生成 JSON 数据。 之后,您使用带有 map[string]interface{} 类型的 json.Unmarshal 函数将 JSON 字符串解码为 Go 数据。 最后,您使用了之前使用 json.Unmarshal 函数定义的 struct 类型,让 Go 根据这些 struct 字段为您进行解析和类型转换。

使用 encoding/json 包,您将能够与 Internet 上提供的许多 API 进行交互,以创建您自己与流行网站的集成。 您还可以将自己程序中的 Go 数据转换为可以保存的格式,然后稍后加载以从程序停止的地方继续。

除了您在本教程中使用的函数之外,encoding/json 包还包括其他可用于与 JSON 交互的有用函数和类型。 例如,json.MarshalIndent 函数可用于 pretty print JSON 数据进行故障排除。

本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。