使用 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)
	}
}

处理 nullboolean 类型

让我们从简单的 JSON 对象开始:nullboolean,都是简单和固定的字符串。

  • 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 行代码,当然也非常简陋。

代码地址:https://github.com/chyroc/go-json-parse-example