gopl-zh.github.com/ch12/ch12-06.md
szh 30e9fc544f
Update ch12-06.md
修正笔误
2021-11-09 15:53:08 +08:00

160 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 12.6. 示例: 解码S表达式
标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如我们在4.5节中看到的要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型§12.3),我们可以这样做:
```Go
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方法返回的记号。
<u><i>gopl.io/ch12/sexpr</i></u>
```Go
type lexer struct {
scan scanner.Scanner
token rune // the current token
}
func (lex *lexer) next() { lex.token = lex.scan.Scan() }
func (lex *lexer) text() string { return lex.scan.TokenText() }
func (lex *lexer) consume(want rune) {
if lex.token != want { // NOTE: Not an example of good error handling.
panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
}
lex.next()
}
```
现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数用于读取S表达式的当前标记然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。
```Go
func read(lex *lexer, v reflect.Value) {
switch lex.token {
case scanner.Ident:
// The only valid identifiers are
// "nil" and struct field names.
if lex.text() == "nil" {
v.Set(reflect.Zero(v.Type()))
lex.next()
return
}
case scanner.String:
s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
v.SetString(s)
lex.next()
return
case scanner.Int:
i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
v.SetInt(int64(i))
lex.next()
return
case '(':
lex.next()
readList(lex, v)
lex.next() // consume ')'
return
}
panic(fmt.Sprintf("unexpected token %q", lex.text()))
}
```
我们的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函数处理。对于mapkey可能是任意类型对元素的处理方式和slice类似我们创建一个新的变量然后递归填充它最后将新解析到的key/value对添加到map。
```Go
func readList(lex *lexer, v reflect.Value) {
switch v.Kind() {
case reflect.Array: // (item ...)
for i := 0; !endList(lex); i++ {
read(lex, v.Index(i))
}
case reflect.Slice: // (item ...)
for !endList(lex) {
item := reflect.New(v.Type().Elem()).Elem()
read(lex, item)
v.Set(reflect.Append(v, item))
}
case reflect.Struct: // ((name value) ...)
for !endList(lex) {
lex.consume('(')
if lex.token != scanner.Ident {
panic(fmt.Sprintf("got token %q, want field name", lex.text()))
}
name := lex.text()
lex.next()
read(lex, v.FieldByName(name))
lex.consume(')')
}
case reflect.Map: // ((key value) ...)
v.Set(reflect.MakeMap(v.Type()))
for !endList(lex) {
lex.consume('(')
key := reflect.New(v.Type().Key()).Elem()
read(lex, key)
value := reflect.New(v.Type().Elem()).Elem()
read(lex, value)
v.SetMapIndex(key, value)
lex.consume(')')
}
default:
panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
}
}
func endList(lex *lexer) bool {
switch lex.token {
case scanner.EOF:
panic("end of file")
case ')':
return true
}
return false
}
```
最后我们将解析器包装为导出的Unmarshal解码函数隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic§5.10然后返回一个对panic对应的错误信息。
```Go
// Unmarshal parses S-expression data and populates the variable
// whose address is in the non-nil pointer out.
func Unmarshal(data []byte, out interface{}) (err error) {
lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
lex.scan.Init(bytes.NewReader(data))
lex.next() // get the first token
defer func() {
// NOTE: this is not an example of ideal error handling.
if x := recover(); x != nil {
err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
}
}()
read(lex, reflect.ValueOf(out).Elem())
return nil
}
```
生产实现不应该对任何输入问题都用panic形式报告而且应该报告一些错误相关的信息例如出现错误输入的行号和位置等。尽管如此我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路以及如何使用反射机制来填充数据结构。
**练习 12.8** sexpr.Unmarshal函数和json.Unmarshal一样都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型支持从一个io.Reader流解码。修改sexpr.Unmarshal函数使用这个新的类型实现。
**练习 12.9** 编写一个基于标记的API用于解码S表达式参考xml.Decoder7.14的风格。你将需要五种类型的标记Symbol、String、Int、StartList和EndList。
**练习 12.10** 扩展sexpr.Unmarshal函数支持布尔型、浮点数和interface类型的解码使用 **练习 12.3** 的方案。提示要解码接口你需要将name映射到每个支持类型的reflect.Type。