跳转到主要内容

标签(标签)

资源精选(342) Go开发(108) Go语言(103) Go(99) angular(82) LLM(75) 大语言模型(63) 人工智能(53) 前端开发(50) LangChain(43) golang(43) 机器学习(39) Go工程师(38) Go程序员(38) Go开发者(36) React(33) Go基础(29) Python(24) Vue(22) Web开发(20) Web技术(19) 精选资源(19) 深度学习(19) Java(18) ChatGTP(17) Cookie(16) android(16) 前端框架(13) JavaScript(13) Next.js(12) 安卓(11) typescript(10) 资料精选(10) NLP(10) 第三方Cookie(9) Redwoodjs(9) LLMOps(9) Go语言中级开发(9) 自然语言处理(9) 聊天机器人(9) PostgreSQL(9) 区块链(9) mlops(9) 安全(9) 全栈开发(8) ChatGPT(8) OpenAI(8) Linux(8) AI(8) GraphQL(8) iOS(8) 软件架构(7) Go语言高级开发(7) AWS(7) C++(7) 数据科学(7) whisper(6) Prisma(6) 隐私保护(6) RAG(6) JSON(6) DevOps(6) 数据可视化(6) wasm(6) 计算机视觉(6) 算法(6) Rust(6) 微服务(6) 隐私沙盒(5) FedCM(5) 语音识别(5) Angular开发(5) 快速应用开发(5) 提示工程(5) Agent(5) LLaMA(5) 低代码开发(5) Go测试(5) gorm(5) REST API(5) 推荐系统(5) WebAssembly(5) GameDev(5) CMS(5) CSS(5) machine-learning(5) 机器人(5) 游戏开发(5) Blockchain(5) Web安全(5) Kotlin(5) 低代码平台(5) 机器学习资源(5) Go资源(5) Nodejs(5) PHP(5) Swift(5) 智能体(4) devin(4) Blitz(4) javascript框架(4) Redwood(4) GDPR(4) 生成式人工智能(4) Angular16(4) Alpaca(4) SAML(4) JWT(4) JSON处理(4) Go并发(4) kafka(4) 移动开发(4) 移动应用(4) security(4) 隐私(4) spring-boot(4) 物联网(4) nextjs(4) 网络安全(4) API(4) Ruby(4) 信息安全(4) flutter(4) 专家智能体(3) Chrome(3) CHIPS(3) 3PC(3) SSE(3) 人工智能软件工程师(3) LLM Agent(3) Remix(3) Ubuntu(3) GPT4All(3) 软件开发(3) 问答系统(3) 开发工具(3) 最佳实践(3) RxJS(3) SSR(3) Node.js(3) Dolly(3) 移动应用开发(3) 编程语言(3) 低代码(3) IAM(3) Web框架(3) CORS(3) 基准测试(3) Go语言数据库开发(3) Oauth2(3) 并发(3) 主题(3) Theme(3) earth(3) nginx(3) 软件工程(3) azure(3) keycloak(3) 生产力工具(3) gpt3(3) 工作流(3) C(3) jupyter(3) 认证(3) prometheus(3) GAN(3) Spring(3) 逆向工程(3) 应用安全(3) Docker(3) Django(3) R(3) .NET(3) 大数据(3) Hacking(3) 渗透测试(3) C++资源(3) Mac(3) 微信小程序(3) Python资源(3) JHipster(3) 大型语言模型(2) 语言模型(2) 可穿戴设备(2) JDK(2) SQL(2) Apache(2) Hashicorp Vault(2) Spring Cloud Vault(2) Go语言Web开发(2) Go测试工程师(2) WebSocket(2) 容器化(2) AES(2) 加密(2) 输入验证(2) ORM(2) Fiber(2) Postgres(2) Gorilla Mux(2) Go数据库开发(2) 模块(2) 泛型(2) 指针(2) HTTP(2) PostgreSQL开发(2) Vault(2) K8s(2) Spring boot(2) R语言(2) 深度学习资源(2) 半监督学习(2) semi-supervised-learning(2) architecture(2) 普罗米修斯(2) 嵌入模型(2) productivity(2) 编码(2) Qt(2) 前端(2) Rust语言(2) NeRF(2) 神经辐射场(2) 元宇宙(2) CPP(2) 数据分析(2) spark(2) 流处理(2) Ionic(2) 人体姿势估计(2) human-pose-estimation(2) 视频处理(2) deep-learning(2) kotlin语言(2) kotlin开发(2) burp(2) Chatbot(2) npm(2) quantum(2) OCR(2) 游戏(2) game(2) 内容管理系统(2) MySQL(2) python-books(2) pentest(2) opengl(2) IDE(2) 漏洞赏金(2) Web(2) 知识图谱(2) PyTorch(2) 数据库(2) reverse-engineering(2) 数据工程(2) swift开发(2) rest(2) robotics(2) ios-animation(2) 知识蒸馏(2) 安卓开发(2) nestjs(2) solidity(2) 爬虫(2) 面试(2) 容器(2) C++精选(2) 人工智能资源(2) Machine Learning(2) 备忘单(2) 编程书籍(2) angular资源(2) 速查表(2) cheatsheets(2) SecOps(2) mlops资源(2) R资源(2) DDD(2) 架构设计模式(2) 量化(2) Hacking资源(2) 强化学习(2) flask(2) 设计(2) 性能(2) Sysadmin(2) 系统管理员(2) Java资源(2) 机器学习精选(2) android资源(2) android-UI(2) Mac资源(2) iOS资源(2) Vue资源(2) flutter资源(2) JavaScript精选(2) JavaScript资源(2) Rust开发(2) deeplearning(2) RAD(2)

介绍


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

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

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

先决条件


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

  • 安装 1.16 或更高版本。要进行设置,请按照您的操作系统的如何安装 Go 教程进行操作。
  • 熟悉 JSON,您可以在 An Introduction to JSON 中找到。
  • 使用 Go 结构标签自定义结构类型字段的能力。更多信息可以在如何在 Go 中使用结构标签中找到。
  • (可选)了解如何在 Go 中创建日期和时间值。您可以在如何在 Go 中使用日期和时间中阅读更多内容。


使用Map生成 JSON


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

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

要开始在程序中使用 encoding/json 包,您需要有一个程序目录。在本教程中,您将使用一个名为 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 函数将地图数据编组为 JSON 数据。

将以下行添加到 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 变量中看到每个值都有一个字符串作为键,但这些键的值各不相同。一个是 int 值,另一个是 bool 值,甚至是另一个 map[string]interface{} 值,其中包含 []int 值。

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

保存并关闭文件。

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

go run main.go


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

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


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

JSON 中的编码时间


encoding/json 包不仅支持字符串和 int 值等类型。它还可以编码更复杂的类型。它支持的更复杂的类型之一是 time 包中的 time.Time 类型。

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

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

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},
        },
    }

    ...
}

此更新会将 2022 年 3 月 2 日的日期和 UTC 时区的上午 9:10:00 时间分配给 dateValue 键。

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

go run main.go


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

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


这次在输出中,您将看到 JSON 数据中的 dateValue 字段,其时间使用 RFC 3339 格式格式化,这是一种用于将日期和时间作为字符串值传达的常用格式。

在 JSON 中编码空值


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

要向 JSON 输出添加几个空值,请再次打开 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,
    }

    ...
}

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

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

go run main.go

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

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


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

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

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

使用结构生成 JSON


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

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

例如,假设您有一个结构体,其中包含一个名为 IntValue 的字段,您将其编组为 JSON:

type myInt struct {
    IntValue int
}

data := &myInt{IntValue: 1234}

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

{"IntValue":1234}


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

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

data := &myInt{IntValue: 1234}

这一次,如果您将数据变量编组为 JSON,json.Marshal 函数将看到 json 结构标记并知道将字段命名为 intValue,因此您将获得预期的结果:

{"intValue":1234}


现在,您将更新您的程序以对 JSON 数据使用结构值。 您将添加一个 myJSON 结构类型来定义您的顶级 JSON 对象,以及一个 myObject 结构来为 ObjectValue 字段定义您的内部 JSON 对象。 您还将向每个字段添加一个 json 结构标记,以告诉 json.Marshal 如何在 JSON 数据中命名它们。 您还需要更新数据变量分配以使用您的 myJSON 结构,声明它类似于您对任何其他 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 值或 nil 值的引用。这就是您可以定义一个深度为多层自定义对象的 JSON 对象的方式。如果您的 JSON 数据需要它,您还可以在 myObject 类型中引用另一个结构类型,依此类推。使用这种模式,您可以使用 Go 结构类型描述非常复杂的 JSON 对象。

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

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

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

go run main.go


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

Output
json 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,因为这是 otherInt 的值。

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

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

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

...

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

...

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

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

go run main.go


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

Output
json 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,字符串的默认 "" 值也被认为是空的,因此它也被排除在外。

在本节中,您更新了程序以使用结构类型生成 JSON 数据,其中包含 json.Marshal 而不是映射类型。您还更新了程序以从 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 变量,如果无法执行,它将返回错误值,如果成功则返回 nil 错误值。在本节中,您将更新您的程序以使用 json.Unmarshal 函数将 JSON 数据从预定义的字符串值读取到映射变量中。您还将更新程序以将 Go 数据打印到输出。

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

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

...

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{} 后,将 jsonData 和 data 传递给 json.Unmarshal 以将 JSON 数据解组到 data 变量中。

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

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

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

go run main.go


输出将类似于以下内容:

Output
json 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 端。您有一个地图值,其中包含来自 JSON 数据的各种字段。您会看到,即使是 JSON 数据中的空字段也会显示在地图中。

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

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

...

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 变量确保 dateValue 字段在地图中。

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

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

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

go run main.go


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

Output
json 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


您可以看到显示从地图中提取并转换为字符串值的 dateValue 字段的日期值行。

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

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

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

使用结构解析 JSON


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

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

现在,打开您的 main.go 文件并更新数据变量声明以使用对 myJSON 结构的引用,并添加几行 fmt.Printf 来显示 myJSON 上各个字段的数据:

...

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)
}

由于您之前定义了结构类型,因此您只需更新数据字段的类型即可支持解组到结构。 其余的更新显示了结构本身中的一些数据。

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

go run main.go


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

Output
json 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 值(time.Date 格式是使用 %#v 时显示的内容格式动词)。由于 Go 能够在 myJSON 的 DateValue 字段中看到 time.Time 类型,因此它也能够为您解析字符串值。

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

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

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

...

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


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

Output
json 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 字段并继续。

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

结论


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

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

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

文章链接