• 周五. 10 月 11th, 2024

    Go语言教程 12.6. 示例: 解码S表达式

    root

    11 月 27, 2021 #Go语言教程

    标准库中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方法返回的记号。

    gopl.io/ch12/sexpr

    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。

    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。

    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对应的错误信息。

    // 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。)

    root