使用 Go 写一个简单的 JSON 解析器
什么是 JSON
JSON 是一个使用文本协议表示的数据,能够表达非常丰富的含义,在现在的 Web 世界应用的非常广泛,一个非常简单的 JSON 文本如下:
{
"id": 24245,
"expired": false,
"book": {
"title": "example title",
"author": {
"name": "jack",
"age": 25
}
},
"discount": null,
"tag": [
"tech",
"news"
]
}
JSON 支持 6 种基本的数据类型,分别是:
- number: 数字,支持整数和浮点数
- string: 双引号包括的字符
- boolean: true 和 false 之一
- array: 由
[
和]
包围的一组对象,每个对象都是一个 JSON 对象,多个对象之间通过,
分隔 - object: 由
{
和}
包围的一组对象,每个对象由 key 和 val 组成,key 必须是字符串类型,val 可以是任意的 JSON 对象,多个 k-v 对之间通过,
分隔 - null: 即 null 这个字符串
编写一个解析 JSON 协议的解析器的思路
JSON 不同对象类型的判断
无论是 pb 还是 thrift,这种在网络中传输的数据的协议一般有这样的特征:
第 1 个字节表示接下来的数据种类,然后 4 个字节表示数据的长度,最后读取特定长度的字节作为数据体。
当然这里的第 1 个字节和 4 个字节都是举例,实际情况可能并非如此。
按照这个思路观察 JSON 支持的 6 个对象类型:
- number: 以
0-9
开头,或以-
开头表示负数 - string: 以
"
开头 - boolean: 以
t
或者f
开头 - array: 以
[
开头 - object: 以
{
开头 - null: 以
n
开头
如何处理空格等数据?
在 JSON 对象的特定位置,是可以忽略空格的,但是在某些位置下,不可以忽略空格。
在关键帧:{
, [
, ,
, :
, ]
, }
附近的空格是没有用的,需要丢弃掉。其他位置的空格需要保留。
解析器的思路
- 取首字节,判断是哪种基本的 JSON 对象类型,调用具体的 JSON 对象解析函数处理数据
- 对于类型:number, string, boolean, null,只需要简单的返回数据即可
- 对于类型: array,在含有
,
的情况下循环调用本函数,得到的结果作为 array 的对象 - 对于类型: map,读取以
:
分隔的 key 和 val 数据,并在含有,
的情况下循环调用,得到的结果作为 map 的对象
代码的编写
思路理清楚了,可以着手写代码了。
parser 实体
我们需要一个实体来记录在 JSON 字符串中寻址的地址(index),让我们来定义一个非常简单的结构体:
其中,data
是输入字符串的 []rune
类型,idx
的范围是 [0, len(s))
,表示当前寻址的位置
type jsonParser struct {
data []rune
idx int
}
解析入口
现在我们什么都没有,我们需要判断这个字符串是哪一种 JSON 对象类型,然后调用子函数来处理它。
注意:此时我们只有上面这个结构体,其他什么都没有,我们在臆想已经有了所有 JSON 对象类型的处理函数之后,可以写这样一个函数来处理 JSON 字符串。
func (r *jsonParser) parse() (interface{}, error) {
if len(r.data) == 0 {
return nil, fmt.Errorf("empty input")
}
itemType := r.data[r.idx]
switch {
case itemType == '"':
res, err := r.parseString()
return res, err
case strings.Contains("0123456789-", string([]rune{itemType})):
res, err := r.parseNumber()
return res, err
case itemType == '{':
res, err := r.parseObject()
return res, err
case itemType == '[':
res, err := r.parseArray()
return res, err
case strings.Contains("tf", string([]rune{itemType})):
res, err := r.parseBoolean()
return res, err
case itemType == 'n':
err := r.parseNull()
return nil, err
default:
return nil, fmt.Errorf("invalid item-type, pos: %d", r.idx)
}
}
null
和 boolean
类型
处理 让我们从简单的 JSON 对象开始:null
和 boolean
,都是简单和固定的字符串。
-
null
的处理是:
func (r *jsonParser) parseNull() error {
if err := r.findRune(false, 'n', 'u', 'l', 'l'); err == nil {
return nil
}
return fmt.Errorf("invalid null-item, pos: %d", r.idx)
}
-
boolean
的处理是:
func (r *jsonParser) parseBoolean() (bool, error) {
if err := r.findRune(false, 't', 'r', 'u', 'e'); err == nil {
return true, nil
}
if err := r.findRune(false, 'f', 'a', 'l', 's', 'e'); err == nil {
return false, nil
}
return false, fmt.Errorf("invalid boolean-item, pos: %d", r.idx)
}
这两个类型的处理是相似的,都是尝试从 r.idx
位置寻找接下来的字符串是不是完全匹配 null
/ true
/ false
如果是,就说明是对应的 JSON 对象,并返回值;
如果不是,则需要报错,表示遇到了类似:tfake
/ nil
这样的非法 JSON 对象。
使用 findRune 函数处理字符串匹配
注意到这里使用了一个函数 findRune
来处理字符串匹配,这是个重要的函数,接下来会解释这个函数。
func (r *jsonParser) findRune(isKey bool, rs ...rune) error {
if isKey {
r.removeSpace()
}
c := 0
for i := r.idx; i < len(r.data) && i-r.idx >= 0 && i-r.idx < len(rs); i++ {
if r.data[i] == rs[i-r.idx] {
c++
continue
}
return fmt.Errorf("expect key rune: %s, pos: %d", string(rs), r.idx)
}
r.idx += c
if isKey {
r.removeSpace()
}
return nil
}
-
findRune
函数接受两个参数:
第一个参数 isKey
表示输入字节是否是一个关键帧,即:{
, [
, ,
, :
, ]
, }
之一,如果是关键帧,则匹配到这个字节前后需要删除空格;
第二个参数 rs
是一个变长 rune
切片,表示需要匹配的字节元素。
-
findRune
函数的处理:
函数的首、尾都使用了 isKey
判断了是否是关键帧,如果是,则执行 remoteSpace
;
在函数的中间部分,遍历了 rs
,并判断是否和输入字符串从 r.idx
位置开始的字符串一致。
- 函数的返回也非常有意思:
首先返回的 error
如果为 nil
,则表示找到了输入的 rs
字节,否则无;
其次,当返回的 error
不为 nil
的时候,r.idx
不会向后移动,这个特性在处理 boolean
类型中,先尝试找 true
,然后尝试找 false
的过程中起到了很好的作用。
移除可以移除的空格
这一部分比较简单,略过,只贴出代码:
func (r *jsonParser) removeSpace() (n int) {
for i := r.idx; i < len(r.data); i++ {
if r.data[i] != ' ' && r.data[i] != '\n' {
return
}
r.idx++
n++
}
return
}
处理 string 类型
下来来我们处理稍微麻烦一点的 string 类型
string 类型的思路就是遍历字符串,当遇到 "
的时候,终止遍历,返回此前遍历的数据,说到这里,是不是觉得又会使用到前面的函数 findRune
了。
不过需要注意的一点是,字符串也可能以转义符 \
包含字符 "
,所以对转义符需要特殊处理下,即遇到转义符的时候,将它的下一个字符直接加入到要返回的数据中。
func (r *jsonParser) parseString() (string, error) {
if err := r.findRune(false, '"'); err != nil {
return "", err
}
res := []rune{}
for r.idx < len(r.data) {
switch r.data[r.idx] {
case '\\':
if r.idx == len(r.data)-1 {
return "", fmt.Errorf("invalid string-item, At least one character is required after the escape character, pos: %d", r.idx)
}
r.idx++
res = append(res, r.data[r.idx])
r.idx++
case '"':
r.idx++
return string(res), nil
default:
res = append(res, r.data[r.idx])
r.idx++
}
}
return "", fmt.Errorf("invalid string-item, expect: \", pos: %d", r.idx)
}
处理 number 类型
number 比 string 类型要更麻烦一点,因为 number 既包括整数,也包括浮点数。
浮点数还涉及到精度的问题,为了简化代码,我们略去了浮点数的处理。
整数可能是正数也可能是负数,所以 number JSON 对象可能以 -
开头,也可能以 0123456789
开头,-
开头表示一个负数。
接下来的处理和 string JSON 对象的处理类似,即遍历字符串,当它是一个 0123456789
之一的时候,将它加入要返回的数据中,否则中断处理。
func (r *jsonParser) parseNumber() (int64, error) {
prefix := r.data[r.idx] == '-'
if prefix {
r.idx++
}
if !strings.Contains("0123456789", string([]rune{r.data[r.idx]})) {
return 0, fmt.Errorf("expect: 0-9, pos: %d", r.idx)
}
i := int64(0)
for r.idx < len(r.data) {
dd := r.data[r.idx]
if dd >= '0' && dd <= '9' {
i = i*10 + int64(dd-'0')
r.idx++
} else {
break
}
}
if prefix {
i *= -1
}
return i, nil
}
处理复合 JSON 对象 array
array 是以 [
和 ]
包括的 JSON 对象的集合,在我们已经处理了 null
/ boolean
/ string
/ number
的情况下,处理 array 非常简单
函数 parseArray
的首尾分别匹配了 [
和 ]
,在函数的中间,循环调用了 parse
来解析 JSON 对象,并加入返回的切片中。
这里有一个比较特别的操作,即在每个循环的最后,会尝试寻找 ,
字符,如果找到,说明还有下一个 JSON array 子元素对象,继续循环,否则结束循环。
func (r *jsonParser) parseArray() (resp []interface{}, err error) {
if err := r.findRune(true, '['); err != nil {
return nil, err
}
if err := r.findRune(true, ']'); err == nil {
return []interface{}{}, nil
}
for {
s, err := r.parse()
if err != nil {
return nil, err
}
resp = append(resp, s)
if err := r.findRune(true, ','); err != nil {
break
}
}
if err := r.findRune(true, ']'); err != nil {
return nil, err
}
return resp, nil
}
处理复合 JSON 对象 object
object 是以 {
和 }
包括的 JSON 对象的键值对集合,和处理 array 的情况比较类似。
函数 parsePbject
的首尾分别匹配了 {
和 }
,在函数的中间,循环调用了 parse
来解析 JSON key 和 val 对象,并加入返回的字典中,注意,key 肯定是一个 string JSON 对象。
和处理 array 比较类似,在本函数的循环中,也会尝试寻找 ,
字符,如果找到,说明还有下一个 JSON object 子元素对象,继续循环,否则结束循环。
func (r *jsonParser) parseObject() (resp map[string]interface{}, err error) {
resp = map[string]interface{}{}
if err := r.findRune(true, '{'); err != nil {
return nil, err
}
if err := r.findRune(true, '}'); err == nil {
return resp, nil
}
for {
key, err := r.parseString()
if err != nil {
return nil, err
}
if err := r.findRune(true, ':'); err != nil {
return nil, err
}
val, err := r.parse()
if err != nil {
return nil, err
}
resp[key] = val
if err := r.findRune(true, ','); err != nil {
break
}
}
if err := r.findRune(true, '}'); err != nil {
return nil, err
}
return resp, nil
}
结语
至此,我们完成了一个简单的 JSON 解析器的编写,它很小,不到 200 行代码,当然也非常简陋。