 第11章 处理JSON数据
第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来存储下一个对象。如果长度太大,则拒绝该输入。
