标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:
Copy 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方法返回的记号。
gopl.io/ch12/sexpr
Copy 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。
Copy 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。
Copy 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对应的错误信息。
Copy // 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。)