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: 3 Imported by: 0

README

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

1. 数字类型的语法

JSON 的数字语法比较简单,比起 C/C++ 的数字字面量,JSON 的数字语法是它的一个子集。语法图如下:

number = [ "-" ] int [ frac ] [ exp ]
int = "0" / digit1-9 *digit
frac = "." 1*digit
exp = ("e" / "E") [ "-" / "+" ] 1*digit

简单翻译一下:

  • 数字可以是负数
  • 整数部分如果不是 0,不能以 0 开头
  • 小数部分是可选的,如果有,必须有小数点和至少一个数字
  • 指数部分是可选的,如果有,必须有 e 或 E,可以有正负号,必须有至少一个数字

比如以下都是合法的 JSON 数字:

0
1
-0
-1
1.1
1e10
1e+10
1e-10
-1e10
-1e+10
-1e-10

但注意以下不是合法的 JSON 数字:

+0     // 不允许正号
+1     // 不允许正号
.123   // 小数点前必须有数字
1.     // 小数点后必须有数字
INF    // 不是合法的 JSON 数字
inf    // 不是合法的 JSON 数字
NAN    // 不是合法的 JSON 数字
nan    // 不是合法的 JSON 数字
0123   // 前导零后不能有数字
0x0    // 不支持十六进制
0x123  // 不支持十六进制

2. 数字的表示方式

在 Go 语言中,我们可以使用 float64 类型来表示 JSON 数字。这是因为 JSON 的数字可以表示整数和浮点数,而 float64 类型可以覆盖大部分常用的数值范围。

我们需要扩展 Value 结构体,添加一个 N 字段来存储数字值:

type Value struct {
	Type ValueType `json:"type"` // 值类型
	N    float64   `json:"n"`    // 数字值(当Type为NUMBER时有效)
}

同时,我们需要添加一个新的错误类型 PARSE_NUMBER_TOO_BIG,用于处理数字溢出的情况:

const (
	PARSE_OK                ParseError = iota // 解析成功
	PARSE_EXPECT_VALUE                        // 期望一个值
	PARSE_INVALID_VALUE                       // 无效的值
	PARSE_ROOT_NOT_SINGULAR                   // 根节点不唯一
	PARSE_NUMBER_TOO_BIG                      // 数字太大
)

3. 数字的解析

数字的解析是本章的重点。我们需要实现 parseNumber 函数,它需要处理各种数字格式,包括整数、小数和指数。

func parseNumber(c *context, v *Value) ParseError {
	// 记录起始位置
	start := c.index

	// 处理负号
	if c.index < len(c.json) && c.json[c.index] == '-' {
		c.index++
	}

	// 处理整数部分
	if c.index < len(c.json) && c.json[c.index] == '0' {
		c.index++
		// 0后面不能直接跟数字,必须是小数点或指数符号
		if c.index < len(c.json) && c.json[c.index] >= '0' && c.json[c.index] <= '9' {
			return PARSE_INVALID_VALUE
		}
	} else if c.index < len(c.json) && c.json[c.index] >= '1' && c.json[c.index] <= '9' {
		c.index++
		for c.index < len(c.json) && c.json[c.index] >= '0' && c.json[c.index] <= '9' {
			c.index++
		}
	} else {
		return PARSE_INVALID_VALUE
	}

	// 处理小数部分
	if c.index < len(c.json) && c.json[c.index] == '.' {
		c.index++
		if c.index >= len(c.json) || c.json[c.index] < '0' || c.json[c.index] > '9' {
			return PARSE_INVALID_VALUE
		}
		for c.index < len(c.json) && c.json[c.index] >= '0' && c.json[c.index] <= '9' {
			c.index++
		}
	}

	// 处理指数部分
	if c.index < len(c.json) && (c.json[c.index] == 'e' || c.json[c.index] == 'E') {
		c.index++
		if c.index < len(c.json) && (c.json[c.index] == '+' || c.json[c.index] == '-') {
			c.index++
		}
		if c.index >= len(c.json) || c.json[c.index] < '0' || c.json[c.index] > '9' {
			return PARSE_INVALID_VALUE
		}
		for c.index < len(c.json) && c.json[c.index] >= '0' && c.json[c.index] <= '9' {
			c.index++
		}
	}

	// 转换为数字
	n, err := strconv.ParseFloat(c.json[start:c.index], 64)
	if err != nil {
		if numError, ok := err.(*strconv.NumError); ok {
			if numError.Err == strconv.ErrRange {
				return PARSE_NUMBER_TOO_BIG
			}
		}
		return PARSE_INVALID_VALUE
	}

	// 检查是否溢出
	if math.IsInf(n, 0) || math.IsNaN(n) {
		return PARSE_NUMBER_TOO_BIG
	}

	v.N = n
	v.Type = NUMBER
	return PARSE_OK
}

解析过程分为几个步骤:

  1. 处理负号:检查是否有负号,如果有则跳过
  2. 处理整数部分:处理0或非0开头的整数
  3. 处理小数部分:处理小数点和小数位
  4. 处理指数部分:处理e/E和指数
  5. 转换为数字:使用Go的strconv.ParseFloat函数将字符串转换为浮点数
  6. 检查溢出:检查数字是否太大

4. 访问数字值

我们需要提供一个函数来访问解析后的数字值:

func GetNumber(v *Value) float64 {
	return v.N
}

5. 单元测试

我们编写了一系列单元测试来验证数字解析功能:

func TestParseNumber(t *testing.T) {
	testCases := []struct {
		input    string
		expected float64
	}{
		{"0", 0.0},
		{"-0", 0.0},
		{"-0.0", 0.0},
		{"1", 1.0},
		{"-1", -1.0},
		{"1.5", 1.5},
		{"-1.5", -1.5},
		{"3.1416", 3.1416},
		{"1E10", 1e10},
		{"1e10", 1e10},
		{"1E+10", 1e+10},
		{"1E-10", 1e-10},
		{"-1E10", -1e10},
		{"-1e10", -1e10},
		{"-1E+10", -1e+10},
		{"-1E-10", -1e-10},
		{"1.234E+10", 1.234e+10},
		{"1.234E-10", 1.234e-10},
		{"1e-10000", 0.0}, // 下溢出
	}

	for _, tc := range testCases {
		t.Run(tc.input, func(t *testing.T) {
			v := Value{}
			if err := Parse(&v, tc.input); err != PARSE_OK {
				t.Errorf("期望解析成功,但返回错误: %v", err)
			}
			if GetType(&v) != NUMBER {
				t.Errorf("期望类型为NUMBER,但得到: %v", GetType(&v))
			}
			if GetNumber(&v) != tc.expected {
				t.Errorf("期望值为%g,但得到: %g", tc.expected, GetNumber(&v))
			}
		})
	}
}

我们还测试了各种无效的数字格式:

func TestParseInvalidValue(t *testing.T) {
	// 测试无效数字
	invalidNumbers := []string{
		"+0",    // 不允许正号
		"+1",    // 不允许正号
		".123",  // 小数点前必须有数字
		"1.",    // 小数点后必须有数字
		"INF",   // 不是合法的JSON数字
		"inf",   // 不是合法的JSON数字
		"NAN",   // 不是合法的JSON数字
		"nan",   // 不是合法的JSON数字
		"0123",  // 前导零后不能有数字
		"0x0",   // 不支持十六进制
		"0x123", // 不支持十六进制
		"0123",  // 不允许前导零
		"1e",    // 指数部分不完整
		"1e+",   // 指数部分不完整
		"1e-",   // 指数部分不完整
	}

	for _, invalidNum := range invalidNumbers {
		t.Run(invalidNum, func(t *testing.T) {
			v := Value{Type: TRUE}
			if err := Parse(&v, invalidNum); err != PARSE_INVALID_VALUE {
				t.Errorf("期望错误PARSE_INVALID_VALUE,但得到: %v,输入: %s", err, invalidNum)
			}
			if GetType(&v) != NULL {
				t.Errorf("期望类型为NULL,但得到: %v", GetType(&v))
			}
		})
	}
}

以及数字太大的情况:

func TestParseNumberTooBig(t *testing.T) {
	v := Value{Type: FALSE}
	if err := Parse(&v, "1e309"); err != PARSE_NUMBER_TOO_BIG {
		t.Errorf("期望错误PARSE_NUMBER_TOO_BIG,但得到: %v", err)
	}
	if GetType(&v) != NULL {
		t.Errorf("期望类型为NULL,但得到: %v", GetType(&v))
	}

	v = Value{Type: FALSE}
	if err := Parse(&v, "-1e309"); err != PARSE_NUMBER_TOO_BIG {
		t.Errorf("期望错误PARSE_NUMBER_TOO_BIG,但得到: %v", err)
	}
	if GetType(&v) != NULL {
		t.Errorf("期望类型为NULL,但得到: %v", GetType(&v))
	}
}

6. 基准测试

为了测试解析器的性能,我们添加了基准测试:

// 基准测试 - 解析null值
func BenchmarkParseNull(b *testing.B) {
	v := Value{}
	for i := 0; i < b.N; i++ {
		Parse(&v, "null")
	}
}

// 基准测试 - 解析true值
func BenchmarkParseTrue(b *testing.B) {
	v := Value{}
	for i := 0; i < b.N; i++ {
		Parse(&v, "true")
	}
}

// 基准测试 - 解析false值
func BenchmarkParseFalse(b *testing.B) {
	v := Value{}
	for i := 0; i < b.N; i++ {
		Parse(&v, "false")
	}
}

// 基准测试 - 解析数字
func BenchmarkParseNumber(b *testing.B) {
	v := Value{}
	for i := 0; i < b.N; i++ {
		Parse(&v, "123.456e+789")
	}
}

运行基准测试的命令如下:

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

从结果可以看出:

  1. 解析 null、true 和 false 的性能非常接近,每次操作大约需要 120 纳秒。
  2. 解析数字的性能相对较慢,每次操作大约需要 435 纳秒,这是因为数字解析涉及更复杂的处理逻辑和字符串到浮点数的转换。
  3. 所有操作都没有内存分配,这是因为我们的解析器设计得非常高效,不需要额外的内存分配。

7. 运行测试

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

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

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

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

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

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

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

# 运行基准测试并显示内存分配信息
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

输出结果解释:

  • 第一列:基准测试名称和CPU核心数
  • 第二列:测试运行的次数
  • 第三列:每次操作的平均时间(纳秒)
  • 第四列:每次操作分配的内存(字节)
  • 第五列:每次操作的内存分配次数

8. 总结

在本章中,我们实现了 JSON 数字的解析功能。主要内容包括:

  1. 了解 JSON 数字的语法规则
  2. 使用 float64 类型表示 JSON 数字
  3. 实现 parseNumber 函数,处理各种数字格式
  4. 添加错误处理,包括处理数字溢出的情况
  5. 编写单元测试和基准测试验证实现
  6. 学习了如何运行测试和基准测试

通过这一章的学习,我们的 JSON 解析器已经可以处理 null、true、false 和数字类型。在下一章中,我们将实现 JSON 字符串的解析,包括处理转义字符和 Unicode 编码。

Documentation

Overview

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

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

数字解析的特殊情况处理: 1. 整数部分:

  • 支持正数和负数
  • 不允许前导零(如"01"是非法的)
  • 不允许十六进制表示(如"0x1"是非法的)
  • 单个"0"是合法的

2. 小数部分:

  • 小数点后必须至少有一个数字(如"1."是非法的)
  • 小数点后不能以0开头(如"1.01"是合法的,但"1.00"会被简化为"1")
  • 小数点后可以有多个数字

3. 指数部分:

  • 支持"e"或"E"作为指数符号
  • 指数部分可以为正数或负数
  • 指数部分不能为空(如"1e"是非法的)
  • 指数部分不能以0开头(如"1e01"是非法的)

4. 边界值处理:

  • 处理数字溢出情况(返回PARSE_NUMBER_TOO_BIG错误)
  • 处理无效数字格式(返回PARSE_INVALID_VALUE错误)

5. 特殊值:

  • 支持科学计数法表示(如"1.23e-4")
  • 支持最大/最小浮点数
  • 支持正负零("0"和"-0")

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetNumber

func GetNumber(v *Value) float64

GetNumber 获取JSON数字值

当且仅当值类型为NUMBER时,返回值有效

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                      // 数字太大
)

解析错误常量

func Parse

func Parse(v *Value, json string) ParseError

Parse 解析JSON文本

接收一个Value指针和JSON字符串,解析JSON并将结果存储在Value中 返回解析过程中可能出现的错误

Example (Error)

示例 - 错误处理

v := Value{}
err := Parse(&v, "invalid")
fmt.Println(err)
Output:
无效的值
Example (False)

示例 - 解析false值

v := Value{}
Parse(&v, "false")
fmt.Println(v.String())
Output:
false
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 (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时有效)
}

Value 表示一个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