Skip to content
程序扫地僧

读取带注释JSON配置文件-GO语言讲解

GO语言, JSON1 min read

为什么不用标准的JSON ?

标准的JSON格式不支持注释以及尾后逗号([Trailing_commas]l, 但注释在将JSON文件作为配置文件时,注释是必要的。而尾后逗号则可以让版本控制diff明确此行没任何修改

尾后逗号 (有时叫做“终止逗号”)在向 JavaScript 代码添加元素、参数、属性时十分有用。如果你想要添加新的属性,并且上一行已经使用了尾后逗号,你可以仅仅添加新的一行,而不需要修改上一行。这使得版本控制的代码比较(diff)更加清晰,代码编辑过程中遇到的麻烦更少

1{
2 "url": "https://www.swordman.xyz",
3 "authors": [
4 "yeluo",
5 "qiuzhi"
6 ], //尾后逗号与注释
7}

如上面的JSON配置, 大多数JSON解释器,都会抛出类似这样的语法错误

1Uncaught SyntaxError: Unexpected token / in JSON at position 81

我们都会想到类似的两种解决方案

  • 重新实现一个解释器,解释JSON的超集(如: json5)

    优点: 全新的解释器,可以实现保留注释,供运行时读取,在开发xx IDE时会比较有用

    缺点: 大量引用了旧API的代码,已经不想去改或者根本就无法修改, 也不符合 开闭原则

  • 解释JSON前删除不合语法的字符(如: JSONConfigReader)

    优点: 通过打补丁或者利用多态,可以无缝替换原有函数,实现简单,原生实现解释器通常性能强劲

    缺点: 注释数据丢失,但一般都不需要运行时注释,还消耗内存

概览

开始解读JSONConfigReader的源码之前,先看一看如何使用以及全部实现(源码),源码分解时不至于云里雾里

使用示例(在线demo):

1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7
8 "github.com/DisposaBoy/JsonConfigReader"
9)
10
11func main() {
12 var v interface{}
13 str := strings.NewReader(`
14 {
15 "url": "https://www.reim.xyz",
16 "authors": [
17 "yeluo",
18 "qiuzhi"
19 ], //尾后逗号与注释
20 }
21 `)
22 // wrap our reader before passing it to the json decoder
23 r := JsonConfigReader.New(str)
24 json.NewDecoder(r).Decode(&v)
25 fmt.Println(v)
26}
27// output: map[authors:[yeluo qiuzhi] url:https://www.reim.xyz]

源码分析

源码比较简单,已经在关键位置都添加了相应注释, 思路主要是把不合法字符全部替换成空格

辅助函数

1// 识别换行符
2func isNL(c byte) bool {
3 return c == '\n' || c == '\r'
4}
1// 识别空格与制表符
2func isWS(c byte) bool {
3 return c == ' ' || c == '\t' || isNL(c)
4}

核心算法

1// 把s变量中的注释、尾后逗号替换成空格
2// 替换成空格相比删除不合法内容更容易实现且性能损耗低
3func prep(r io.Reader) (s []byte, err error) {
4 buf := &bytes.Buffer{}
5 // 复制原始数据
6 _, err = io.Copy(buf, r)
7 s = buf.Bytes()
8 if err != nil {
9 return
10 }
11
12 i := 0
13 // 遍历全部字符
14 for i < len(s) {
15 switch s[i] {
16 // 遇到字符串开始标志, 开启内部循环跳过
17 case '"':
18 i += 1
19 for i < len(s) {
20 // 遇到字符串结束标志,退出内部循环
21 if s[i] == '"' {
22 i += 1
23 break
24 // 忽略字符串转义
25 // 两次 i += 1 等于 i += 2
26 // 跳过\与紧接着\的第一个字符
27 } else if s[i] == '\\' {
28 i += 1
29 }
30 i += 1
31 }
32 // 可能是注释开始标志,执行转交给注释处理程序
33 case '/':
34 i = consumeComment(s, i+1)
35 // 可能是尾后逗号,处理尾后逗号
36 case ',':
37 // 记录逗号位置
38 j := i
39 // 开始内部循环,跳过空格、制表符与注释
40 for {
41 i += 1
42 if i >= len(s) {
43 break
44 // 下一个合法字符是数组或对象结束标志
45 // 把记录下来的尾后逗号替换成空格
46 } else if s[i] == '}' || s[i] == ']' {
47 s[j] = ' '
48 break
49 // 可能是注释开始标志,执行转交给注释处理程序
50 } else if s[i] == '/' {
51 i = consumeComment(s, i+1)
52 } else if !isWS(s[i]) {
53 break
54 }
55 }
56 // 其它情况不需要处理,直接跳过该字符
57 default:
58 i += 1
59 }
60 }
61 return
62}
1// 接下来的内容如果是注释,则全部替换成空格,
2// 不是注释则不做任何处理
3// 返回处理结束后下标所在位置
4func consumeComment(s []byte, i int) int {
5 // 如果遇到行内注释, 从 "//" 到行结束全部换成空格
6 if i < len(s) && s[i] == '/' {
7 s[i-1] = ' '
8 for ; i < len(s) && !isNL(s[i]); i += 1 {
9 s[i] = ' '
10 }
11 }
12 // 如果遇到块级注释, 从"/*"到"*/"全部换成空格
13 if i < len(s) && s[i] == '*' {
14 s[i-1] = ' '
15 s[i] = ' '
16 for ; i < len(s); i += 1 {
17 // 不是*全部换成空格
18 if s[i] != '*' {
19 s[i] = ' '
20 // 遇到*
21 } else {
22 s[i] = ' '
23 // 移动到下一字符
24 i++
25 if i < len(s) {
26 // *后面是/, 即"*/",跳出循环
27 if s[i] == '/' {
28 s[i] = ' '
29 break
30 }
31 }
32 }
33 }
34 }
35 return i
36}
1// 创建state结构
2type state struct {
3 r io.Reader
4 br *bytes.Reader
5}
6// 为state结构实现io.Reader接口
7func (st *state) Read(p []byte) (n int, err error) {
8 // 第一次读取时才处理数据,节省不必要性能开支
9 if st.br == nil {
10 var s []byte
11 if s, err = prep(st.r); err != nil {
12 return
13 }
14 st.br = bytes.NewReader(s)
15 }
16 return st.br.Read(p)
17}
18
19// 创建一个实现了io.Reader接口的state结构对象, 作为原始Reader的代理
20func New(r io.Reader) io.Reader {
21 return &state{r: r}
22}

利用多态特性,实现一个io.Reader的透明代理,可以兼容所有使用io.Reader读取数据的JSON解释库

© 2022 by 程序扫地僧. All rights reserved.
粤ICP备2022042070号-1
使用Gatsby构建