mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-11-28 09:09:07 +00:00
160 lines
7.8 KiB
Markdown
160 lines
7.8 KiB
Markdown
## 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函数处理。对于map,key可能是任意类型,对元素的处理方式和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.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。
|
||
|
||
**练习 12.10:** 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 **练习 12.3:** 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)
|