CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
  • 第2章 字符串处理
  • 第3章 处理日期和时间
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
    • 编组/解组基础
    • 结构体编码
      • 操作方法……
    • 处理嵌入结构体
      • 操作方法……
    • 不定义结构体进行编码
      • 操作方法……
    • 结构体解码
      • 操作方法……
    • 使用接口、映射和切片进行解码
      • 如何操作...
    • 其他解码数字的方式
      • 如何操作...
      • 处理缺失值和可选值
    • 编码时省略空字段
      • 如何操作...
    • 解码时处理缺失字段
      • 如何操作...
      • 自定义JSON编码/解码
    • 自定义数据类型的编组/解组
      • 如何操作...
      • 对象键的自定义编组/解组
      • 如何操作...
      • 动态字段名
      • 如何操作...
      • 多态数据结构
      • 两遍自定义反序列化
      • 如何实现……
      • 流式处理JSON数据
      • 流式输出对象数组
      • 如何实现……
      • 解析对象数组
      • 如何实现……
      • 其他流式处理JSON的方式
      • 安全注意事项
      • 如何实现……
  • 第12章 进程
  • 第13章 网络编程
  • 第14章 流式输入/输出
  • 第15章 数据库
  • 第16章 日志记录
  • 第17章 测试、基准测试和性能分析
目录

第11章 处理JSON数据

# 第11章 处理JSON数据

JSON是JavaScript对象表示法(JavaScript Object Notation)的缩写。它是一种流行的数据交换格式,因为JSON对象与结构化类型(Go语言中的结构体)非常相似,并且它是基于文本的编码方式,使得编码后的数据易于阅读。它支持数组、对象(键值对)以及相对较少的基本类型(字符串、数字、布尔值和null)。这些特性使得JSON成为一种相当容易处理的格式。

编码(Encoding)是指将数据元素转换为字节序列的过程。当你用JSON对数据元素进行编码(或编组,marshal)时,你要遵循JSON语法规则,创建这些数据元素的文本表示形式。相反的过程,解码(decoding,或解组,unmarshaling)则是将JSON值赋给Go语言对象。编码过程是有信息损失的:你必须将数据值描述为文本,而对于复杂数据类型来说,这并不总是显而易见的。当你解码这类数据时,你必须知道如何解释文本表示形式,以便正确解析JSON表示。

在本章中,我们将首先了解基本数据类型的编码和解码。然后,我们将研究一些处理更复杂数据类型和用例的方法。在实现自己的解决方案时,你可以将这些方法作为指南。这些方法展示了特定用例的解决方案,你可能需要根据自己的特定需求对其进行调整。

本章包含以下方法:

  • 结构体编码
  • 处理嵌入结构体
  • 不定义结构体进行编码
  • 结构体解码
  • 使用接口、映射和切片进行解码
  • 其他解码数字的方法
  • 自定义数据类型的编组/解组
  • 对象键的自定义编组/解组
  • 动态字段名
  • 多态数据结构
  • 流式处理JSON数据

# 编组/解组基础

标准库的encoding/json包提供了方便的函数和约定,用于编码/解码JSON数据。

# 结构体编码

Go语言的结构体类型通常被编码为JSON对象。本节展示标准库中处理数据类型编码的工具。

# 操作方法……

  1. 使用JSON标签为结构体字段标注它们在JSON中的键:
type Config struct {
    Version string `json:"ver"`  			// 编码为"ver"
    Name    string               			// 编码为"Name"
    Type    string `json:"type,omitempty"` 	// 编码为"type",如果为空则省略
    Style   string `json:"-"`    			// 不编码
    value   string               			// 未导出字段,不编码
    kind    string `json:"kind"` 			// 未导出字段,不编码
}
1
2
3
4
5
6
7
8
  1. 使用json.Marshal函数将Go语言数据对象编码为JSON。标准库对基本类型使用以下约定: | Go语言声明 | 值 | JSON输出 | | ---------------------------------------------- | ------- | ------------------------------------------------------------ | | NumberValue int json:"num" | 0 | "num": 0 | | NumberValue *int json:"num" | nil | "num": null | | NumberValue *int json:"num,omitempty" | nil | 省略 | | BoolValue bool json:"bvalue" | true | "bvalue": true | | BoolValue *bool json:"bvalue" | nil | "bvalue": null | | BoolValue *bool json:"bvalue,omitempty" | nil | 省略 | | StringValue string json:"svalue" | "str" | "svalue":"str" | | StringValue string json:"svalue" | "" | "svalue":"" | | StringValue string json:"svalue,omitempty" | "str" | "svalue":"str" | | StringValue string json:"svalue,omitempty" | "" | 省略 | | StringValue *string json:"svalue" | nil | "svalue": null | | StringValue *string json:"svalue,omitempty" | nil | 省略 | | 结构体和映射类型 | - | 编码为JSON对象 | | 切片和数组类型 | - | 编码为JSON数组 | | 如果一个类型实现了json.Marshaler接口 | - | 调用变量实例的json.Marshaler.MarshalJSON方法对数据进行编码 | | 如果一个类型实现了encoding.TextMarshaler接口 | - | 该值被编码为JSON字符串,字符串值通过调用该值的encoding.TextMarshaler.MarshalText方法获得 | | 其他情况 | - | 会出现UnsupportedValueError错误 |
提示
只有结构体类型的导出字段才能被编组。
提示
如果结构体字段没有JSON标签,其JSON对象键将与字段名相同。

考虑以下代码片段:

type Config struct {
    Version string `json:"ver"`  				// 编码为"ver"
    Name    string               				// 编码为"Name"
    Type    string `json:"type,omitempty"` 		// 编码为"type",如果为空则省略
    Style   string `json:"-"`    				// 不编码
    value   string               				// 未导出字段,不编码
    kind    string `json:"kind"` 				// 未导出字段,不编码
}

...

cfg := Config{
    Version: "1.1",
    Name:    "name",
    Type:    "example",
    Style:   "json",
    value:   "example config value",
    kind:    "test",
}

data, err := json.Marshal(cfg)
fmt.Println(string(err))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这段代码的输出如下:

{"ver":"1.1","Name":"name","type":"example"}
1
提示
编码后的JSON对象中字段的顺序与字段声明的顺序相同。

# 处理嵌入结构体

一个结构体类型的结构体字段将被编码为JSON对象。如果存在嵌入结构体,那么编码器有两种选择:将嵌入结构体与包含它的结构体编码在同一层级,或者编码为一个新的JSON对象。

# 操作方法……

  1. 使用JSON标签为包含结构体的字段和嵌入结构体的字段命名:
type Enclosing struct {
    Field    string `json:"field"`
    Embedded
}

type Embedded struct {
    Field string `json:"embeddedField"`
}
1
2
3
4
5
6
7
8
  1. 使用json.Marshal将结构体编码为JSON对象:
enc := Enclosing{
    Field: "enclosing",
    Embedded: Embedded{
        Field: "embedded",
    },
}

data, err = json.Marshal(enc)
// {"field":"enclosing","embeddedField":"embedded"}
1
2
3
4
5
6
7
8
9
  1. 给嵌入结构体添加JSON标签将创建一个嵌套的JSON对象:
type Enclosing struct {
    Field    string `json:"field"`
    Embedded `json:"embedded"`
}

type Embedded struct {
    Field string `json:"embeddedField"`
}

...

enc := Enclosing{
    Field: "enclosing",
    Embedded: Embedded{
        Field: "embedded",
    },
}

data, err = json.Marshal(enc)
// {"field":"enclosing","embedded":{"embeddedField":"embedded"}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 不定义结构体进行编码

基本数据类型、切片和映射可用于编码JSON数据。

# 操作方法……

  • 使用映射表示JSON对象:
config := map[string]any{
    "ver":  "1.0",
    "Name": "config",
    "type": "example",
}

data, err := json.Marshal(config)
// {"ver":"1.0","Name":"config","type":"example"}
1
2
3
4
5
6
7
8
  • 使用切片表示JSON数组:
numbersWithNil := []any{1, 2, nil, 3}
data, err := json.Marshal(numbersWithNil)
// [1,2,null,3]
1
2
3
  • 使期望的JSON结构与Go语言中的等效结构相匹配:
configurations := map[string]map[string]any{
    "cfg1": {
        "ver":  "1.0",
        "Name": "config1",
    },
    "cfg2": {
        "ver":  "1.1",
        "Name": "config2",
    },
}
data, err := json.Marshal(configurations)
// {"cfg1":{"Name":"config1","ver":"1.0"},"cfg2":{"Name":"config2","ver":"1.1"}}
1
2
3
4
5
6
7
8
9
10
11
12

# 结构体解码

将Go语言数据对象编码为JSON是一项相对容易的任务:定义明确的数据类型和语义被转换为表达性稍弱的表示形式,通常会导致一些信息丢失。例如,一个整数变量和一个float64变量编码后的输出可能是相同的。因此,解码JSON数据通常更困难。

# 操作方法……

  1. 使用JSON标签将JSON键映射到结构体字段。
  2. 使用json.Unmarshal函数将JSON数据解码为Go语言数据对象。标准库对基本类型使用以下约定: | JSON输入 | Go语言类型 | 结果 | | ------------------------------------------------------------ | -------------------- | ------------------------------------------------------------ | | "strValue" | string | "strValue" | | 1(数字) | int | 1 | | 1.2(数字) | int | 错误 | | 1.2(数字) | float64, float32 | 1.2 | | true | bool | true | | null | string | 变量保持不变 | | null | int | 变量保持不变 | | "strValue" | *string | "strValue" | | null | *string | nil | | 1 | *int | 1 | | null | *int | nil | | true | *bool | true | | null | *bool | nil | | 如果Go语言类型是interface{} | - | 标准库按以下约定创建对象: | | JSON输入 | 结果 | | | "strValue" | "strValue" | | | 1 | float64(1) | | | 1.2 | float64(1.2) | | | true | true | | | null | nil | | | JSON对象 | map[string]any | | | JSON数组 | []any | | | 如果目标Go语言类型实现了json.Unmarshaler接口 | - | 调用json.Unmarshal.UnmarshalJSON对数据进行解码。如有必要,此操作可能涉及创建目标类型的新实例 | | 如果目标Go语言类型实现了encoding.TextUnmarshaler接口,且输入是带引号的JSON字符串 | - | 调用encoding.TextUnmarshaler.UnmarshalText对值进行解码 | | 其他情况 | - | 会出现UnsupportedValueError错误 |
提示
如果JSON输入包含各种数值类型的值,可能会造成混淆。例如,如果将一个JSON数值解组到一个int值中,如果JSON数据可以表示为整数则没问题,但如果JSON数据是浮点值则会失败。
提示
JSON解码器永远不会更改结构体的未导出字段。解码器使用反射,而通过反射只能访问导出字段。
提示
没有匹配Go语言字段的JSON字段将被忽略。

# 使用接口、映射和切片进行解码

在将Go语言的值解码为JSON时,Go语言的值类型决定了JSON编码的方式。JSON没有像Go语言那样丰富的类型系统。有效的JSON类型有字符串、数字、布尔值、对象、数组和null。当你将JSON数据解码到Go语言的结构体中时,仍然是由Go语言的类型系统来决定如何解释JSON数据。但是当你将JSON数据解码到interface{}中时,情况就变了。此时是由JSON数据决定如何构建Go语言的值,这有时会导致意想不到的结果。

# 如何操作...

要将JSON数据反序列化到接口中,可以使用以下方法:

var output interface{}
err: = json.Unmarshal(jsonData, &output)
1
2

这会根据以下转换规则创建一个对象树:

JSON Go
对象 map[string]interface{}
数组 []interface{}
数字 float64
布尔值 bool
字符串 string
空值 nil

# 其他解码数字的方式

当解码到interface{}中时,JSON数字会被转换为float64。但这并不总是符合预期。你可以使用json.Number来替代。

# 如何操作...

使用json.Decoder并调用UseNumber方法:

var output interface{}
decoder:=json.NewDecoder(strings.NewReader(`[1.1,2,3,4.4]`))
// 告诉解码器使用json.Number而不是float64
decoder.UseNumber()
err:=decoder.Decode(&output)
// [1.1 2 3 4.4]
1
2
3
4
5
6

在前面的例子中,output的每个元素都是json.Number的实例。你可以根据需要将其转换为int、float64或big.Int 。

# 处理缺失值和可选值

通常,你需要处理包含缺失字段的JSON输入,并且在生成JSON时要省略空字段。本节将介绍如何处理这些情况。

# 编码时省略空字段

在JSON编码中省略空字段通常可以节省空间,并且使JSON更易于阅读。不过,“空” 的定义应该明确。

# 如何操作...

使用,omitempty JSON标签来省略空字符串值、零整数/浮点数、零time.Duration值和nil指针值。

,omitempty标签对time.Time值不起作用。可以使用*time.Time并将其设置为nil来省略空的时间值:

type Config struct {
...
Type       string        `json:"type,omitempty"`
IntValue   int     		 `json:"intValue,omitempty"`
FloatValue float64       `json:"floatValue,omitempty"`
When       *time.Time    `json:"when,omitempty"`
HowLong    time.Duration `json:"howLong,omitempty"`
}
1
2
3
4
5
6
7
8

有时,区分空字符串和空值字符串(null字符串)很重要。在JavaScript和JSON中,null是字符串的有效取值。如果是这种情况,可以使用*string:

type Config struct {
	Value  *string `json:"value,omitempty"`
	...
}

...

emptyString := ""
emptyValue := Config {
	Value: &emptyString,
}

// JSON输出: { "value": "" }
nullValue := Config {
	Value: nil,
}

// JSON输出: {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 解码时处理缺失字段

在一些场景中,开发人员必须处理不包含所有数据字段的稀疏JSON数据。例如,部分更新的API调用可能会接受一个JSON对象,该对象只包含需要更新的字段,而不修改任何未指定的数据字段。在这种情况下,识别哪些字段被提供就变得很重要。还有些场景中,为缺失字段设置默认值是合理的。

# 如何操作...

如果你想确定JSON输入中指定了哪些字段,可以使用指针字段。输入中缺失的任何字段将保持为nil。

为缺失字段提供默认值,可以在反序列化之前将这些字段初始化为它们的默认值:

type APIRequest struct {
    // 如果未指定type,它将为nil
    Type    *string `json:"type"`
    // seq将有一个默认值
    Seq     int     `json:"seq"`
    ...
}

func handler(w http.ResponseWriter,r *http.Request) {
    data, err:=io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request",http.StatusBadRequest)
        return
	}
    
    req:=APIRequest{
    	Seq: 1,  // 设置默认值
	}
    
    if err: = json.Unmarshal(data, &req); err! = nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    
    // 检查提供了哪些字段
    if req.Type! = nil {
    	...
    }
    
    // 如果输入中提供了seq,req.Seq将被设置为该值。否则,它将为1。
    if req.Seq == 1 {
    ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 自定义JSON编码/解码

有时,某些数据结构的JSON编码与它们在程序中的表示形式不匹配。遇到这种情况时,你必须自定义特定数据元素编码为JSON或从JSON解码的方式。

# 自定义数据类型的编组/解组

当数据元素的JSON表示需要通过编程生成时,可以使用这些方法。

# 如何操作...

要控制数据对象在JSON中的编码方式,可以实现json.Marshaler接口:

// TypeAndID编码为JSON时的格式为type:id
type TypeAndID struct {
    Type string
    ID string
}

// json.Marshaler接口的实现
func (t TypeAndID) MarshalJSON() (out []byte, err error) {
    s := fmt.Sprintf("%s:%d", t.Type, t.ID)
    out = []byte(s)
    return
}
1
2
3
4
5
6
7
8
9
10
11
12

要控制数据对象从JSON的解码方式,可以实现json.Unmarshaler接口:

提示
解组器必须使用指针接收器。
// json.Unmarshaler接口的实现。注意使用指针接收器
func (t *TypeAndID) UnmarshalJSON(in []byte) (err error) {
    parts := strings.Split(string(in),":")
    if len(parts) != 2 {
    	err=ErrInvalidTypeAndID
    	return
	}
    
    // 第二部分必须是有效的整数
    t.ID, err=strconv.Atoi(parts[1])
    if err != nil {
    	return
    }
    
    t.Type=parts[0]
    return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 对象键的自定义编组/解组

映射在编组/解组时会被处理为JSON对象。但是,如果你的映射键不是字符串类型,该如何将其编组/解组为JSON呢?

# 如何操作...

解决方案取决于键的确切类型:

  1. 键类型派生自字符串或整数类型的映射,可以使用标准库方法进行编组/解组:
type Key int64

func main() {
    var m map[Key]int
    err := json.Unmarshal([]byte(`{"123":123}`), &m)
    if err != nil {
    	panic(err)
    }
    
    fmt.Println(m[123]) // 输出123
}
1
2
3
4
5
6
7
8
9
10
11
  1. 如果映射键在编组/解组时需要额外处理,可以实现encoding.TextMarshaler和encoding.TextUnmarshaler接口:
// Key是一个uint类型,在JSON键中编码为十六进制字符串
type Key uint

func (k *Key) UnmarshalText(data []byte) error {
    v, err := strconv.ParseInt(string(data), 16, 64)
    if err != nil {
    	return err
    }
    
    *k = Key(v)
    
    return nil
}
    
func (k Key) MarshalText() ([]byte, error) {
    s := strconv.FormatUint(uint64(k), 16)
    return []byte(s), nil
}

func main() {
    input := `{
    	"13AD": "5037",  "3E22": "15906", "90A3": "37027"
    	}`
    
    var data map[Key]string
    if err := json.Unmarshal([]byte(input), &data); err != nil
    {
    	panic(err)
    }
    
    fmt.Println(data)
    
    d, err := json.Marshal(map[Key]any{
        Key(123): "123",
        Key(255): "255",
    })
    
    if err != nil {
    	panic(err)
    }
    
    fmt.Println(string(d))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 动态字段名

在某些情况下,字段名(对象键)不是固定的。例如,一个API可能更倾向于将对象列表作为JSON对象返回,其中每个对象的唯一标识符作为键。在这种情况下,无法在结构体中使用JSON标签。

# 如何操作...

使用map[string]ValueType来表示具有动态字段名的对象:

type User struct {
	Name string `json:"name"`
	Type string `json:"type"`
}

type Users struct {
	Users map[string]User `json:"users"`
}

func main() {
    input := `{
        "users": {
            "abb64dfe-d4a8-47a5-b7b0-7613fe3fd11f": {
            "name": "John",
            "type": "admin"
        },
        "b158161c-0588-4c67-8e4b-c07a8978f711": {
            "name": "Amy",
            "type": "editor"
            }
        }
    }`
    
    var users Users
    if err := json.Unmarshal([]byte(input), &users); err != nil {
    	panic(err)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 多态数据结构

多态数据结构可以是共享同一个接口的几种不同类型之一。实际类型在运行时确定。对于运行时对象,Go语言的类型系统使用这样的字段来确保类型安全的操作。通过使用接口,多态对象可以很容易地编组为JSON。但当需要反序列化多态JSON对象时,就会出现问题。在本方法中,我们将探讨实现这一目标的不同方式。

# 两遍自定义反序列化

第一遍反序列化鉴别器字段,同时保留输入的其余部分不处理。基于鉴别器,构建并反序列化对象的具体实例。

# 如何实现……

  1. 本节我们将以一个Key结构体示例进行讲解。Key结构体用于存储不同类型的加密公钥,其类型由Type字段指定:
type KeyType string

const (
    KeyTypeRSA     = "rsa"
    KeyTypeED25519 = "ed25519"
)

type Key struct {
    Type KeyType          `json:"type"`
    Key  crypto.PublicKey `json:"key"`
}
1
2
3
4
5
6
7
8
9
10
11
  1. 像往常一样为数据结构定义JSON标签。大多数多态结构在没有自定义序列化器的情况下也能进行序列化,因为在序列化过程中对象的运行时类型是已知的。定义另一个结构体,它是原始结构体的副本,将其中动态类型的部分替换为json.RawMessage类型的字段:
type keyUnmarshal struct {
    Type KeyType         `json:"type"`
    Key  json.RawMessage `json:"key"`
}
1
2
3
4
  1. 为原始结构体创建一个反序列化器。在这个反序列化器中,首先将输入反序列化为步骤2中创建的结构体实例:
func (k *Key) UnmarshalJSON(in []byte) error {
    var key keyUnmarshal
    err := json.Unmarshal(in, &key)
    if err != nil {
        return err
    }
1
2
3
4
5
6
  1. 使用类型鉴别器字段,决定如何解码动态部分。以下示例使用一个工厂来获取特定类型的反序列化器:
k.Type = key.Type
unmarshaler := KeyUnmarshalers[key.Type]
if unmarshaler == nil {
    return ErrInvalidKeyType
}
1
2
3
4
5
  1. 将动态类型部分(即json.RawMessage)反序列化为正确类型的变量实例:
k.Key, err = unmarshaler(key.Key)
if err != nil {
    return err
}

return nil
1
2
3
4
5
6

工厂是一个简单的映射,它知道不同类型密钥的反序列化器:

var (
    KeyUnmarshalers = map[KeyType]func(json.RawMessage) (crypto.PublicKey, error){}
)

func RegisterKeyUnmarshaler(keyType KeyType, unmarshaler func(json.RawMessage) (crypto.PublicKey, error)) {
    KeyUnmarshalers[keyType] = unmarshaler
}

...

RegisterKeyUnmarshaler(KeyTypeRSA, func(in json.RawMessage) (crypto.PublicKey, error) {
    var key rsa.PublicKey
    if err := json.Unmarshal(in, &key); err != nil {
        return nil, err
    }
    
    return &key, nil
})

RegisterKeyUnmarshaler(KeyTypeED25519, func(in json.RawMessage) (crypto.PublicKey, error) {
    var key ed25519.PublicKey
    if err := json.Unmarshal(in, &key); err != nil {
        return nil, err
    }
    
    return &key, nil
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这是一个可扩展的工厂框架,可以使用在构建时确定的其他反序列化器进行初始化。只需为一种对象类型创建一个反序列化器函数,并使用前面的RegisterKeyUnmarshaler函数进行注册,即可支持新的密钥类型。

提示
注册这类功能的常用方法是使用包的init()函数。当你导入该包时,该包支持的反序列化器类型将被注册。

# 流式处理JSON数据

当需要高效处理大量数据时,应考虑流式处理数据,而不是一次性处理整个数据集。本节介绍一些流式处理JSON数据的方法。

# 流式输出对象数组

如果你有一个生成器(如goroutine、数据库游标等)能生成数据元素,并且希望将这些元素作为JSON数组进行流式输出,而不是在序列化之前存储所有内容,那么本方法会很有用。

# 如何实现……

  1. 创建一个生成器。它可以是:
    • 通过通道发送数据元素的goroutine;
    • 包含Next()方法的类似游标的对象;
    • 或者其他数据生成器。
  2. 使用表示目标的io.Writer创建一个json.Encoder实例。目标可以是文件、标准输出、缓冲区、网络连接等。
  3. 写入数组起始分隔符,即[。
  4. 对每个数据元素进行编码,必要时在前面加上逗号。
  5. 写入数组结束分隔符,即]。

以下示例假设存在一个生成器goroutine,将Data实例写入输入通道。当没有更多Data实例时,生成器会关闭通道。这里假设Data是可进行JSON序列化的:

func stream(out io.Writer, input <-chan Data) error {
    enc := json.NewEncoder(out)
    if _, err := out.Write([]byte{'['}); err != nil {
        return err
    }
    
    first := true
    
    for obj := range input {
        if first {
            first = false
        } else {
            if _, err := out.Write([]byte{','}); err != nil {
                return err
            }
        }
        
        if err := enc.Encode(obj); err != nil {
            return err
        }
    }
    
    if _, err := out.Write([]byte{']'}); err != nil {
        return err
    }
    
    return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 解析对象数组

如果你有一个提供对象数组的JSON数据源,可以使用json.Decoder解析这些元素并进行处理。

# 如何实现……

  1. 创建一个从输入流读取数据的json.Decoder。
  2. 使用json.Decoder.Token()解析数组起始分隔符([)。
  3. 对数组中的每个元素进行解码,直到解码失败。
  4. 当解码失败时,你必须确定是流结束了,还是真的存在错误。要检查这一点,可以使用json.Decoder.Token()读取下一个标记。如果成功读取下一个标记,并且它是数组结束分隔符],那么流解析成功结束。否则,输入数据存在错误。

以下示例假设json.Decoder已经构建好,用于从输入流读取数据。输出存储在一个切片中。或者,也可以在解析元素时对输出进行处理,或者将每个元素通过通道发送到一个处理goroutine:

func parse(input *json.Decoder) (output []Data, err error) {
    // 解析数组起始分隔符
    var tok json.Token
    tok, err = input.Token()
    if err != nil {
        return
    }
    
    if tok != json.Delim('[') {
        err = fmt.Errorf("Array begin delimiter expected")
        return
    }
    
    // 使用Decode解析数组元素
    for {
        var data Data
        err = input.Decode(&data)
        if err != nil {
            // 解码失败。可能是输入错误,也可能是流结束了
            tok, err = input.Token()
            if err != nil {
                // 数据错误
                return
            }
            
            // 是流结束了吗?
            if tok == json.Delim(']') {
                // 是的,没有错误
                err = nil
                break
            }
        }
        
        output = append(output, data)
    }
    
    return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 其他流式处理JSON的方式

还有其他流式处理JSON的方式:

  • 连接式JSON(Concatenated JSON):简单地将JSON对象一个接一个地写入。
  • 换行符分隔的JSON(Newline-delimited JSON):将每个JSON对象作为单独的一行写入。
  • 记录分隔符分隔的JSON(Record separator-delimited JSON):使用特殊的记录分隔符0x1E,并且每个JSON对象之间可选地添加换行符。
  • 长度前缀式JSON(Length-prefixed JSON):将每个JSON对象的字符串长度作为十进制数前缀。

所有这些都可以使用json.Decoder和json.Encoder进行读取和写入。一个简单的JSON流式处理包可以在以下链接找到:https://github.com/bserdar/jsonstream (opens new window)。

# 安全注意事项

每当从应用程序外部接收数据(用户输入的数据、API调用、读取文件等)时,都必须关注恶意输入。JSON输入相对安全,因为JSON解析器不会像YAML或XML解析器那样进行数据扩展。尽管如此,在处理JSON数据时仍有一些需要考虑的事项。

# 如何实现……

在接受第三方JSON输入时限制数据量。不要盲目使用io.ReadAll或json.Decode:

const MessageSizeLimit = 10240
func handler(w http.ResponseWriter, r *http.Request) {
    reader := http.MaxBytesReader(w, r.Body, MessageSizeLimit)
    data, err := io.ReadAll(reader)
    if errors.Is(err, &http.MaxBytesError{}) {
        // 如果发生这种情况,错误已经被发送。
        return
    }
   ...
}
1
2
3
4
5
6
7
8
9
10
  • 始终根据从第三方输入读取的数据为资源分配设置上限。例如,如果你正在读取长度前缀式JSON流,其中每个JSON对象都前缀了其长度,不要分配[]byte来存储下一个对象。如果长度太大,则拒绝该输入。
第10章 处理大量数据
第12章 进程

← 第10章 处理大量数据 第12章 进程→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式