示例: 解码S表达式
标准库中 encoding/… 下每个包中提供的 Marshal 编码函数都有一个对应的 Unmarshal 函数用于解码。例如,我们在4.5节中看到的,要将包含 JSON 编码格式的字节 slice 数据解码为我们自己的 Movie 类型(§12.3),我们可以这样做:
data := []byte{/* ... */}
var movie Movie
err := json.Unmarshal(data, &movie)
Unmarshal 函数使用了反射机制类修改 movie 变量的每个成员,根据输入的内容为 Movie 成员创建对应的 map、结构体和 slice。
现在让我们为 S 表达式编码实现一个简易的 Unmarshal,类似于前面的 json.Unmarshal 标准库函数,对应我们之前实现的 sexpr.Marshal 函数的逆操作。我们必须提醒一下,一个健壮的和通用的实现通常需要比例子更多的代码,为了便于演示我们采用了精简的实现。我们只支持 S 表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的是为了演示反射的用法,而不是构造一个实用的 S 表达式的解码器。
词法分析器 lexer 使用了标准库中的 text/scanner 包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器 scanner 的 Scan 方法将提前扫描和返回下一个记号,对于 rune 类型。大多数记号,比如“(”,对应一个单一 rune 可表示的 Unicode 字符,但是 text/scanner 也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用 Scan 方法将返回这些记号的类型,接着调用 TokenText 方法将返回记号对应的文本内容。
因为每个解析器可能需要多次使用当前的记号,但是 Scan 会一直向前扫描,所以我们包装了一个 lexer 扫描器辅助类型,用于跟踪最近由 Scan 方法返回的记号。
Unresolved include directive in modules/ROOT/pages/ch12/ch12-06.adoc - include::example$/ch12/sexpr/decode.go[]
现在让我们转到语法解析器。它主要包含两个功能。第一个是 read 函数,用于读取 S 表达式的当前标记,然后根据 S 表达式的当前标记更新可取地址的 reflect.Value 对应的变量 v 。
Unresolved include directive in modules/ROOT/pages/ch12/ch12-06.adoc - include::example$/ch12/sexpr/decode.go[]
我们的S表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符,我们都作为错误处理。后面的readList函数将处理结构体的成员名。
一个“(”标记对应一个列表的开始。第二个函数readList,将一个列表解码到一个聚合类型中(map、结构体、slice或数组),具体类型依赖于传入待填充变量的类型。每次遇到这种情况,循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”,endList函数用于检测结束标记。
最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。
在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。
Unresolved include directive in modules/ROOT/pages/ch12/ch12-06.adoc - include::example$/ch12/sexpr/decode.go[]
最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。
Unresolved include directive in modules/ROOT/pages/ch12/ch12-06.adoc - include::example$/ch12/sexpr/decode.go[]
生产实现不应该对任何输入问题都用panic形式报告,而且应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。
练习 12.8: sexpr.Unmarshal函数和json.Unmarshal一样,都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型,支持从一个io.Reader流解码。修改sexpr.Unmarshal函数,使用这个新的类型实现。
练习 12.9: 编写一个基于标记的API用于解码S表达式,参考xml.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。
练习 12.10: 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 练习 12.3: 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)