读取带注释JSON配置文件-GO语言讲解
为什么不用标准的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 main2
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 decoder23 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 return10 }11
12 i := 013 // 遍历全部字符14 for i < len(s) {15 switch s[i] {16 // 遇到字符串开始标志, 开启内部循环跳过17 case '"':18 i += 119 for i < len(s) {20 // 遇到字符串结束标志,退出内部循环21 if s[i] == '"' {22 i += 123 break24 // 忽略字符串转义25 // 两次 i += 1 等于 i += 226 // 跳过\与紧接着\的第一个字符27 } else if s[i] == '\\' {28 i += 129 }30 i += 131 }32 // 可能是注释开始标志,执行转交给注释处理程序33 case '/':34 i = consumeComment(s, i+1)35 // 可能是尾后逗号,处理尾后逗号36 case ',':37 // 记录逗号位置38 j := i39 // 开始内部循环,跳过空格、制表符与注释40 for {41 i += 142 if i >= len(s) {43 break44 // 下一个合法字符是数组或对象结束标志45 // 把记录下来的尾后逗号替换成空格46 } else if s[i] == '}' || s[i] == ']' {47 s[j] = ' '48 break49 // 可能是注释开始标志,执行转交给注释处理程序50 } else if s[i] == '/' {51 i = consumeComment(s, i+1)52 } else if !isWS(s[i]) {53 break54 }55 }56 // 其它情况不需要处理,直接跳过该字符57 default:58 i += 159 }60 }61 return62}
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 break30 }31 }32 }33 }34 }35 return i36}
1// 创建state结构2type state struct {3 r io.Reader4 br *bytes.Reader5}6// 为state结构实现io.Reader接口7func (st *state) Read(p []byte) (n int, err error) {8 // 第一次读取时才处理数据,节省不必要性能开支9 if st.br == nil {10 var s []byte11 if s, err = prep(st.r); err != nil {12 return13 }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解释库