12.6. 示例: 解码S表达式

标准库中encoding/…下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:

  1. data := []byte{/* ... */}
  2. var movie Movie
  3. 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方法返回的记号。

gopl.io/ch12/sexpr

  1. type lexer struct {
  2. scan scanner.Scanner
  3. token rune // the current token
  4. }
  5. func (lex *lexer) next() { lex.token = lex.scan.Scan() }
  6. func (lex *lexer) text() string { return lex.scan.TokenText() }
  7. func (lex *lexer) consume(want rune) {
  8. if lex.token != want { // NOTE: Not an example of good error handling.
  9. panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
  10. }
  11. lex.next()
  12. }

现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标记,然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。

  1. func read(lex *lexer, v reflect.Value) {
  2. switch lex.token {
  3. case scanner.Ident:
  4. // The only valid identifiers are
  5. // "nil" and struct field names.
  6. if lex.text() == "nil" {
  7. v.Set(reflect.Zero(v.Type()))
  8. lex.next()
  9. return
  10. }
  11. case scanner.String:
  12. s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
  13. v.SetString(s)
  14. lex.next()
  15. return
  16. case scanner.Int:
  17. i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
  18. v.SetInt(int64(i))
  19. lex.next()
  20. return
  21. case '(':
  22. lex.next()
  23. readList(lex, v)
  24. lex.next() // consume ')'
  25. return
  26. }
  27. panic(fmt.Sprintf("unexpected token %q", lex.text()))
  28. }

我们的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。

  1. func readList(lex *lexer, v reflect.Value) {
  2. switch v.Kind() {
  3. case reflect.Array: // (item ...)
  4. for i := 0; !endList(lex); i++ {
  5. read(lex, v.Index(i))
  6. }
  7. case reflect.Slice: // (item ...)
  8. for !endList(lex) {
  9. item := reflect.New(v.Type().Elem()).Elem()
  10. read(lex, item)
  11. v.Set(reflect.Append(v, item))
  12. }
  13. case reflect.Struct: // ((name value) ...)
  14. for !endList(lex) {
  15. lex.consume('(')
  16. if lex.token != scanner.Ident {
  17. panic(fmt.Sprintf("got token %q, want field name", lex.text()))
  18. }
  19. name := lex.text()
  20. lex.next()
  21. read(lex, v.FieldByName(name))
  22. lex.consume(')')
  23. }
  24. case reflect.Map: // ((key value) ...)
  25. v.Set(reflect.MakeMap(v.Type()))
  26. for !endList(lex) {
  27. lex.consume('(')
  28. key := reflect.New(v.Type().Key()).Elem()
  29. read(lex, key)
  30. value := reflect.New(v.Type().Elem()).Elem()
  31. read(lex, value)
  32. v.SetMapIndex(key, value)
  33. lex.consume(')')
  34. }
  35. default:
  36. panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
  37. }
  38. }
  39. func endList(lex *lexer) bool {
  40. switch lex.token {
  41. case scanner.EOF:
  42. panic("end of file")
  43. case ')':
  44. return true
  45. }
  46. return false
  47. }

最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。

  1. // Unmarshal parses S-expression data and populates the variable
  2. // whose address is in the non-nil pointer out.
  3. func Unmarshal(data []byte, out interface{}) (err error) {
  4. lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
  5. lex.scan.Init(bytes.NewReader(data))
  6. lex.next() // get the first token
  7. defer func() {
  8. // NOTE: this is not an example of ideal error handling.
  9. if x := recover(); x != nil {
  10. err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
  11. }
  12. }()
  13. read(lex, reflect.ValueOf(out).Elem())
  14. return nil
  15. }

生产实现不应该对任何输入问题都用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。)