第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对象。本节展示标准库中处理数据类型编码的工具。
# 操作方法……
- 使用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"` // 未导出字段,不编码
}
2
3
4
5
6
7
8
- 使用
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))
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"}
提示 编码后的JSON对象中字段的顺序与字段声明的顺序相同。 |
---|
# 处理嵌入结构体
一个结构体类型的结构体字段将被编码为JSON对象。如果存在嵌入结构体,那么编码器有两种选择:将嵌入结构体与包含它的结构体编码在同一层级,或者编码为一个新的JSON对象。
# 操作方法……
- 使用JSON标签为包含结构体的字段和嵌入结构体的字段命名:
type Enclosing struct {
Field string `json:"field"`
Embedded
}
type Embedded struct {
Field string `json:"embeddedField"`
}
2
3
4
5
6
7
8
- 使用
json.Marshal
将结构体编码为JSON对象:
enc := Enclosing{
Field: "enclosing",
Embedded: Embedded{
Field: "embedded",
},
}
data, err = json.Marshal(enc)
// {"field":"enclosing","embeddedField":"embedded"}
2
3
4
5
6
7
8
9
- 给嵌入结构体添加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"}}
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"}
2
3
4
5
6
7
8
- 使用切片表示JSON数组:
numbersWithNil := []any{1, 2, nil, 3}
data, err := json.Marshal(numbersWithNil)
// [1,2,null,3]
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"}}
2
3
4
5
6
7
8
9
10
11
12
# 结构体解码
将Go语言数据对象编码为JSON是一项相对容易的任务:定义明确的数据类型和语义被转换为表达性稍弱的表示形式,通常会导致一些信息丢失。例如,一个整数变量和一个float64
变量编码后的输出可能是相同的。因此,解码JSON数据通常更困难。
# 操作方法……
- 使用JSON标签将JSON键映射到结构体字段。
- 使用
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)
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]
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"`
}
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输出: {}
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 {
...
}
}
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
}
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 对象键的自定义编组/解组
映射在编组/解组时会被处理为JSON对象。但是,如果你的映射键不是字符串类型,该如何将其编组/解组为JSON呢?
# 如何操作...
解决方案取决于键的确切类型:
- 键类型派生自字符串或整数类型的映射,可以使用标准库方法进行编组/解组:
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
}
2
3
4
5
6
7
8
9
10
11
- 如果映射键在编组/解组时需要额外处理,可以实现
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))
}
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)
}
}
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对象时,就会出现问题。在本方法中,我们将探讨实现这一目标的不同方式。
# 两遍自定义反序列化
第一遍反序列化鉴别器字段,同时保留输入的其余部分不处理。基于鉴别器,构建并反序列化对象的具体实例。
# 如何实现……
- 本节我们将以一个
Key
结构体示例进行讲解。Key
结构体用于存储不同类型的加密公钥,其类型由Type
字段指定:
type KeyType string
const (
KeyTypeRSA = "rsa"
KeyTypeED25519 = "ed25519"
)
type Key struct {
Type KeyType `json:"type"`
Key crypto.PublicKey `json:"key"`
}
2
3
4
5
6
7
8
9
10
11
- 像往常一样为数据结构定义JSON标签。大多数多态结构在没有自定义序列化器的情况下也能进行序列化,因为在序列化过程中对象的运行时类型是已知的。定义另一个结构体,它是原始结构体的副本,将其中动态类型的部分替换为
json.RawMessage
类型的字段:
type keyUnmarshal struct {
Type KeyType `json:"type"`
Key json.RawMessage `json:"key"`
}
2
3
4
- 为原始结构体创建一个反序列化器。在这个反序列化器中,首先将输入反序列化为步骤2中创建的结构体实例:
func (k *Key) UnmarshalJSON(in []byte) error {
var key keyUnmarshal
err := json.Unmarshal(in, &key)
if err != nil {
return err
}
2
3
4
5
6
- 使用类型鉴别器字段,决定如何解码动态部分。以下示例使用一个工厂来获取特定类型的反序列化器:
k.Type = key.Type
unmarshaler := KeyUnmarshalers[key.Type]
if unmarshaler == nil {
return ErrInvalidKeyType
}
2
3
4
5
- 将动态类型部分(即
json.RawMessage
)反序列化为正确类型的变量实例:
k.Key, err = unmarshaler(key.Key)
if err != nil {
return err
}
return nil
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
})
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数组进行流式输出,而不是在序列化之前存储所有内容,那么本方法会很有用。
# 如何实现……
- 创建一个生成器。它可以是:
- 通过通道发送数据元素的goroutine;
- 包含
Next()
方法的类似游标的对象; - 或者其他数据生成器。
- 使用表示目标的
io.Writer
创建一个json.Encoder
实例。目标可以是文件、标准输出、缓冲区、网络连接等。 - 写入数组起始分隔符,即
[
。 - 对每个数据元素进行编码,必要时在前面加上逗号。
- 写入数组结束分隔符,即
]
。
以下示例假设存在一个生成器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
}
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
解析这些元素并进行处理。
# 如何实现……
- 创建一个从输入流读取数据的
json.Decoder
。 - 使用
json.Decoder.Token()
解析数组起始分隔符([
)。 - 对数组中的每个元素进行解码,直到解码失败。
- 当解码失败时,你必须确定是流结束了,还是真的存在错误。要检查这一点,可以使用
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
}
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
}
...
}
2
3
4
5
6
7
8
9
10
- 始终根据从第三方输入读取的数据为资源分配设置上限。例如,如果你正在读取长度前缀式JSON流,其中每个JSON对象都前缀了其长度,不要分配
[]byte
来存储下一个对象。如果长度太大,则拒绝该输入。