leptjson

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2025 License: MIT Imports: 4 Imported by: 0

README

从零开始的 JSON 库教程(五):解析数组 (Go语言版)

本文是《从零开始的 JSON 库教程》的第五个单元。本单元的源代码位于 json-tutorial/go-json-tutorial/tutorial05

本单元内容:

  1. JSON 数组语法
  2. 数组表示
  3. 解析数组
  4. 内存管理
  5. 总结与练习
  6. 参考

1. JSON 数组语法

JSON 数组是一个有序的值的集合,以方括号 [] 包围,值之间以逗号 , 分隔。完整的语法如下:

array = %x5B ws [ value *( ws %x2C ws value ) ] ws %x5D

简单来说,一个数组可以包含零个或多个值,值可以是任何 JSON 值类型(null、false、true、数字、字符串、数组、对象)。

例如:

  • 空数组:[]
  • 包含单个值的数组:[null]
  • 包含多个值的数组:[null, false, true, 123, "abc"]
  • 嵌套数组:[[1, 2], [3, 4], 5]

2. 数组表示

在 Go 语言中,我们可以使用切片来表示 JSON 数组。我们在 Value 结构体中添加了一个 A 字段来存储数组值:

// Value 表示一个JSON值
type Value struct {
    Type   ValueType `json:"type"`   // 值类型
    N      float64   `json:"n"`      // 数字值(当Type为NUMBER时有效)
    S      string    `json:"s"`      // 字符串值(当Type为STRING时有效)
    A      []*Value  `json:"a"`      // 数组值(当Type为ARRAY时有效)
}

同时,我们添加了两个函数来操作数组:

// GetArraySize 获取JSON数组的大小
func GetArraySize(v *Value) int {
    return len(v.A)
}

// GetArrayElement 获取JSON数组的元素
func GetArrayElement(v *Value, index int) *Value {
    if index < 0 || index >= len(v.A) {
        return nil
    }
    return v.A[index]
}

3. 解析数组

解析 JSON 数组的过程相对简单:

  1. 跳过开头的 [
  2. 如果遇到 ],表示是空数组
  3. 否则,解析第一个值
  4. 然后循环处理:
    • 如果遇到 ],表示数组结束
    • 如果遇到 ,,则解析下一个值
    • 否则,返回错误

以下是解析数组的核心代码:

// parseArray 解析数组值
func parseArray(c *context, v *Value) ParseError {
    c.index++ // 跳过开头的 [
    parseWhitespace(c)
    
    // 处理空数组的情况
    if c.index < len(c.json) && c.json[c.index] == ']' {
        c.index++
        v.Type = ARRAY
        v.A = make([]*Value, 0)
        return PARSE_OK
    }
    
    var elements []*Value
    
    for {
        // 解析数组元素
        element := &Value{}
        if err := parseValue(c, element); err != PARSE_OK {
            return err
        }
        elements = append(elements, element)
        
        parseWhitespace(c)
        if c.index >= len(c.json) {
            return PARSE_MISS_COMMA_OR_SQUARE_BRACKET
        }
        
        // 检查是否到达数组结束或需要继续解析
        if c.json[c.index] == ']' {
            c.index++
            v.Type = ARRAY
            v.A = elements
            return PARSE_OK
        } else if c.json[c.index] == ',' {
            c.index++
            parseWhitespace(c)
        } else {
            return PARSE_MISS_COMMA_OR_SQUARE_BRACKET
        }
    }
}

我们还需要在 parseValue 函数中添加对数组的处理:

// parseValue 解析JSON值
func parseValue(c *context, v *Value) ParseError {
    if c.index >= len(c.json) {
        return PARSE_EXPECT_VALUE
    }

    switch c.json[c.index] {
    case 'n':
        return parseNull(c, v)
    case 't':
        return parseTrue(c, v)
    case 'f':
        return parseFalse(c, v)
    case '"':
        return parseString(c, v)
    case '[':
        return parseArray(c, v)
    case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        return parseNumber(c, v)
    default:
        return PARSE_INVALID_VALUE
    }
}

4. 内存管理

在 Go 语言中,内存管理由垃圾回收器自动处理,这使得我们不需要手动释放内存。但是,我们仍然需要注意一些内存使用的问题:

  1. 切片的容量:当我们使用 append 向切片添加元素时,如果超出当前容量,Go 会自动分配更大的底层数组。为了减少内存分配次数,我们可以预先分配足够的容量。

  2. 指针的使用:我们使用 []*Value 而不是 []Value 来存储数组元素,这样可以避免在复制 Value 结构体时的开销,特别是当 Value 结构体变得更大时(例如,当我们添加对象支持后)。

  3. 递归深度:解析嵌套数组时,我们使用递归调用 parseValue。在极端情况下,这可能导致栈溢出。在实际应用中,我们可能需要限制递归深度或使用非递归算法。

5. 总结与练习

本单元我们实现了 JSON 数组的解析,包括处理嵌套数组和各种错误情况。Go 语言的切片和垃圾回收机制使得实现相对简单,但我们仍然需要注意内存使用和性能优化。

练习:

  1. 实现一个 SetArray 函数,允许设置 Value 的数组值。
  2. 修改 parseArray 函数,预先分配一定容量的切片,比较性能差异。
  3. 实现数组的生成功能,将 Value 中的数组值转换为 JSON 字符串。
  4. 添加一个 DeepCopy 函数,用于深度复制 Value 结构体,包括其中的数组。
  5. 实现一个 FreeValue 函数,用于释放 Value 结构体占用的内存(虽然在 Go 中通常不需要手动释放内存,但这是一个很好的练习)。

6. 参考

  1. RFC 7159 - The JavaScript Object Notation (JSON) Data Interchange Format
  2. Go 切片:用法和内部实现
  3. Go 内存管理
  4. Go 垃圾回收

7. 运行测试

单元测试

在Go语言中,测试是通过内置的go test命令来运行的。要运行我们的JSON库测试,可以使用以下命令:

# 在tutorial05目录下运行所有测试
go test

# 运行测试并显示详细输出
go test -v

# 运行特定的测试函数
go test -run TestParseArray
基准测试

基准测试用于测量代码的性能。要运行基准测试,可以使用以下命令:

# 运行所有基准测试
go test -bench=.

# 运行特定的基准测试
go test -bench=BenchmarkParseArray

# 运行基准测试并显示内存分配信息
go test -bench=. -benchmem

基准测试的输出示例:

BenchmarkParseNull-8           10000000               118 ns/op              0 B/op          0 allocs/op
BenchmarkParseTrue-8           10000000               119 ns/op              0 B/op          0 allocs/op
BenchmarkParseFalse-8          10000000               120 ns/op              0 B/op          0 allocs/op
BenchmarkParseNumber-8          3000000               435 ns/op              0 B/op          0 allocs/op
BenchmarkParseString-8          2000000               756 ns/op             32 B/op          1 allocs/op
BenchmarkParseUnicodeString-8   1000000              1254 ns/op             48 B/op          2 allocs/op
BenchmarkParseArray-8            500000              3254 ns/op            224 B/op          9 allocs/op

从结果可以看出:

  1. 解析数组比解析基本类型和字符串要慢,这是因为数组解析涉及更复杂的处理逻辑和更多的内存分配。
  2. 数组解析会导致多次内存分配,这是因为我们需要为数组元素分配内存,并且可能需要多次扩展切片容量。

8. 常见问题

  1. 为什么使用[]*Value而不是[]Value来存储数组元素?

    使用指针切片可以避免在复制 Value 结构体时的开销,特别是当 Value 结构体变得更大时(例如,当我们添加对象支持后)。此外,使用指针可以更方便地处理递归结构(如嵌套数组)。

  2. 解析嵌套数组有什么挑战?

    解析嵌套数组的主要挑战是处理递归结构。在我们的实现中,parseArray 函数会递归调用 parseValue 函数来解析数组元素,而 parseValue 函数又可能调用 parseArray 函数来解析嵌套数组。这种递归结构需要小心处理,以避免栈溢出。

  3. 为什么需要 PARSE_MISS_COMMA_OR_SQUARE_BRACKET 错误?

    这个错误用于处理两种情况:

    • 解析数组元素后,期望遇到逗号或右方括号,但遇到了其他字符
    • 解析到数组末尾,但缺少右方括号

    通过这个错误,我们可以提供更具体的错误信息,帮助用户定位问题。

  4. 如何优化数组解析的性能?

    优化数组解析性能的几种方法:

    • 预分配切片容量,减少动态扩容的次数
    • 使用迭代而不是递归来解析嵌套结构,避免栈溢出
    • 使用内存池来重用 Value 结构体,减少内存分配
    • 对于大型数组,考虑使用流式解析,而不是一次性加载整个数组

9. 下一步

在下一个单元中,我们将实现 JSON 对象的解析,这是 JSON 中最复杂的数据结构。我们将学习如何使用哈希表来存储对象的键值对,以及如何处理对象的嵌套结构。

Documentation

Overview

leptjson.go - Go语言版JSON库实现

这个包实现了一个简单的JSON解析器,支持基本的JSON值类型 包括null、true、false、数字、字符串和数组等。后续版本将支持对象。

数组解析的特殊情况处理: 1. 基本规则:

  • 数组必须以方括号[]开始和结束
  • 数组元素之间必须用逗号分隔
  • 空数组[]是合法的
  • 数组末尾不能有多余的逗号(如[1,2,]是非法的)

2. 元素处理:

  • 支持任意类型的JSON值作为元素
  • 元素之间可以有任意数量的空白字符
  • 元素可以是null、true、false、数字、字符串或数组

3. 嵌套数组:

  • 支持任意深度的数组嵌套
  • 每个嵌套数组都必须遵循相同的规则
  • 嵌套数组的解析使用递归方式

4. 错误处理:

  • PARSE_MISS_COMMA_OR_SQUARE_BRACKET: 缺少逗号或右方括号
  • PARSE_INVALID_VALUE: 无效的数组元素
  • PARSE_EXPECT_VALUE: 期望一个值

5. 性能考虑:

  • 使用切片动态增长数组
  • 预分配合理的初始容量
  • 避免频繁的内存分配

6. 边界情况:

  • 处理空数组[]
  • 处理单个元素的数组[1]
  • 处理嵌套空数组[[]]
  • 处理混合类型数组[1,"string",null,true]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetArraySize

func GetArraySize(v *Value) int

GetArraySize 获取JSON数组的大小

func GetNumber

func GetNumber(v *Value) float64

GetNumber 获取JSON数字值

func GetString

func GetString(v *Value) string

GetString 获取JSON字符串值

Types

type ParseError

type ParseError int

ParseError 表示解析过程中可能出现的错误

const (
	PARSE_OK                           ParseError = iota // 解析成功
	PARSE_EXPECT_VALUE                                   // 期望一个值
	PARSE_INVALID_VALUE                                  // 无效的值
	PARSE_ROOT_NOT_SINGULAR                              // 根节点不唯一
	PARSE_NUMBER_TOO_BIG                                 // 数字太大
	PARSE_MISS_QUOTATION_MARK                            // 缺少引号
	PARSE_INVALID_STRING_ESCAPE                          // 无效的转义序列
	PARSE_INVALID_STRING_CHAR                            // 无效的字符
	PARSE_INVALID_UNICODE_HEX                            // 无效的Unicode十六进制
	PARSE_INVALID_UNICODE_SURROGATE                      // 无效的Unicode代理对
	PARSE_MISS_COMMA_OR_SQUARE_BRACKET                   // 缺少逗号或方括号
)

解析错误常量

func Parse

func Parse(v *Value, json string) ParseError

Parse 解析JSON文本

Example (Array)

示例 - 解析数组

v := Value{}
Parse(&v, "[1,2,3]")
fmt.Println(v.String())
Output:
[1,2,3]
Example (Error)

示例 - 错误处理

v := Value{}
err := Parse(&v, "[1,2,")
fmt.Println(err)
Output:
缺少逗号或方括号
Example (False)

示例 - 解析false值

v := Value{}
Parse(&v, "false")
fmt.Println(v.String())
Output:
false
Example (NestedArray)

示例 - 解析嵌套数组

v := Value{}
Parse(&v, "[[1,2],[3,4],5]")
fmt.Println(v.String())
Output:
[[1,2],[3,4],5]
Example (Null)

示例 - 解析null值

v := Value{}
Parse(&v, "null")
fmt.Println(v.String())
Output:
null
Example (Number)

示例 - 解析数字

v := Value{}
Parse(&v, "123.456")
fmt.Println(v.String())
Output:
123.456
Example (String)

示例 - 解析字符串

v := Value{}
Parse(&v, "\"Hello, World!\"")
fmt.Println(v.String())
Output:
Hello, World!
Example (True)

示例 - 解析true值

v := Value{}
Parse(&v, "true")
fmt.Println(v.String())
Output:
true

func (ParseError) Error

func (e ParseError) Error() string

Error 实现error接口,返回错误描述

type Value

type Value struct {
	Type ValueType `json:"type"` // 值类型
	N    float64   `json:"n"`    // 数字值(当Type为NUMBER时有效)
	S    string    `json:"s"`    // 字符串值(当Type为STRING时有效)
	A    []*Value  `json:"a"`    // 数组值(当Type为ARRAY时有效)
}

Value 表示一个JSON值

func GetArrayElement

func GetArrayElement(v *Value, index int) *Value

GetArrayElement 获取JSON数组的元素

func (*Value) String

func (v *Value) String() string

String 实现fmt.Stringer接口,返回Value的字符串表示

type ValueType

type ValueType int

ValueType 表示JSON值的类型

const (
	NULL   ValueType = iota // null值
	FALSE                   // false值
	TRUE                    // true值
	NUMBER                  // 数字
	STRING                  // 字符串
	ARRAY                   // 数组
	OBJECT                  // 对象
)

JSON值类型常量

func GetType

func GetType(v *Value) ValueType

GetType 获取JSON值的类型

func (ValueType) String

func (t ValueType) String() string

String 返回ValueType的字符串表示

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL