Go 語言 示例: 解碼S表達(dá)式

2023-03-14 17:00 更新

原文鏈接:https://gopl-zh.github.io/ch12/ch12-06.html


12.6. 示例: 解碼S表達(dá)式

標(biāo)準(zhǔn)庫中encoding/...下每個(gè)包中提供的Marshal編碼函數(shù)都有一個(gè)對應(yīng)的Unmarshal函數(shù)用于解碼。例如,我們在4.5節(jié)中看到的,要將包含JSON編碼格式的字節(jié)slice數(shù)據(jù)解碼為我們自己的Movie類型(§12.3),我們可以這樣做:

data := []byte{/* ... */}
var movie Movie
err := json.Unmarshal(data, &movie)

Unmarshal函數(shù)使用了反射機(jī)制類修改movie變量的每個(gè)成員,根據(jù)輸入的內(nèi)容為Movie成員創(chuàng)建對應(yīng)的map、結(jié)構(gòu)體和slice。

現(xiàn)在讓我們?yōu)镾表達(dá)式編碼實(shí)現(xiàn)一個(gè)簡易的Unmarshal,類似于前面的json.Unmarshal標(biāo)準(zhǔn)庫函數(shù),對應(yīng)我們之前實(shí)現(xiàn)的sexpr.Marshal函數(shù)的逆操作。我們必須提醒一下,一個(gè)健壯的和通用的實(shí)現(xiàn)通常需要比例子更多的代碼,為了便于演示我們采用了精簡的實(shí)現(xiàn)。我們只支持S表達(dá)式有限的子集,同時(shí)處理錯(cuò)誤的方式也比較粗暴,代碼的目的是為了演示反射的用法,而不是構(gòu)造一個(gè)實(shí)用的S表達(dá)式的解碼器。

詞法分析器lexer使用了標(biāo)準(zhǔn)庫中的text/scanner包將輸入流的字節(jié)數(shù)據(jù)解析為一個(gè)個(gè)類似注釋、標(biāo)識(shí)符、字符串面值和數(shù)字面值之類的標(biāo)記。輸入掃描器scanner的Scan方法將提前掃描和返回下一個(gè)記號(hào),對于rune類型。大多數(shù)記號(hào),比如“(”,對應(yīng)一個(gè)單一rune可表示的Unicode字符,但是text/scanner也可以用小的負(fù)數(shù)表示記號(hào)標(biāo)識(shí)符、字符串等由多個(gè)字符組成的記號(hào)。調(diào)用Scan方法將返回這些記號(hào)的類型,接著調(diào)用TokenText方法將返回記號(hào)對應(yīng)的文本內(nèi)容。

因?yàn)槊總€(gè)解析器可能需要多次使用當(dāng)前的記號(hào),但是Scan會(huì)一直向前掃描,所以我們包裝了一個(gè)lexer掃描器輔助類型,用于跟蹤最近由Scan方法返回的記號(hào)。

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()
}

現(xiàn)在讓我們轉(zhuǎn)到語法解析器。它主要包含兩個(gè)功能。第一個(gè)是read函數(shù),用于讀取S表達(dá)式的當(dāng)前標(biāo)記,然后根據(jù)S表達(dá)式的當(dāng)前標(biāo)記更新可取地址的reflect.Value對應(yīng)的變量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表達(dá)式使用標(biāo)識(shí)符區(qū)分兩個(gè)不同類型,結(jié)構(gòu)體成員名和nil值的指針。read函數(shù)值處理nil類型的標(biāo)識(shí)符。當(dāng)遇到scanner.Ident為“nil”時(shí),使用reflect.Zero函數(shù)將變量v設(shè)置為零值。而其它任何類型的標(biāo)識(shí)符,我們都作為錯(cuò)誤處理。后面的readList函數(shù)將處理結(jié)構(gòu)體的成員名。

一個(gè)“(”標(biāo)記對應(yīng)一個(gè)列表的開始。第二個(gè)函數(shù)readList,將一個(gè)列表解碼到一個(gè)聚合類型中(map、結(jié)構(gòu)體、slice或數(shù)組),具體類型依賴于傳入待填充變量的類型。每次遇到這種情況,循環(huán)繼續(xù)解析每個(gè)元素直到遇到于開始標(biāo)記匹配的結(jié)束標(biāo)記“)”,endList函數(shù)用于檢測結(jié)束標(biāo)記。

最有趣的部分是遞歸。最簡單的是對數(shù)組類型的處理。直到遇到“)”結(jié)束標(biāo)記,我們使用Index函數(shù)來獲取數(shù)組每個(gè)元素的地址,然后遞歸調(diào)用read函數(shù)處理。和其它錯(cuò)誤類似,如果輸入數(shù)據(jù)導(dǎo)致解碼器的引用超出了數(shù)組的范圍,解碼器將拋出panic異常。slice也采用類似方法解析,不同的是我們將為每個(gè)元素創(chuàng)建新的變量,然后將元素添加到slice的末尾。

在循環(huán)處理結(jié)構(gòu)體和map每個(gè)元素時(shí)必須解碼一個(gè)(key value)格式的對應(yīng)子列表。對于結(jié)構(gòu)體,key部分對于成員的名字。和數(shù)組類似,我們使用FieldByName找到結(jié)構(gòu)體對應(yīng)成員的變量,然后遞歸調(diào)用read函數(shù)處理。對于map,key可能是任意類型,對元素的處理方式和slice類似,我們創(chuàng)建一個(gè)新的變量,然后遞歸填充它,最后將新解析到的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
}

最后,我們將解析器包裝為導(dǎo)出的Unmarshal解碼函數(shù),隱藏了一些初始化和清理等邊緣處理。內(nèi)部解析器以panic的方式拋出錯(cuò)誤,但是Unmarshal函數(shù)通過在defer語句調(diào)用recover函數(shù)來捕獲內(nèi)部panic(§5.10),然后返回一個(gè)對panic對應(yīng)的錯(cuò)誤信息。

// 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
}

生產(chǎn)實(shí)現(xiàn)不應(yīng)該對任何輸入問題都用panic形式報(bào)告,而且應(yīng)該報(bào)告一些錯(cuò)誤相關(guān)的信息,例如出現(xiàn)錯(cuò)誤輸入的行號(hào)和位置等。盡管如此,我們希望通過這個(gè)例子來展示類似encoding/json等包底層代碼的實(shí)現(xiàn)思路,以及如何使用反射機(jī)制來填充數(shù)據(jù)結(jié)構(gòu)。

練習(xí) 12.8: sexpr.Unmarshal函數(shù)和json.Unmarshal一樣,都要求在解碼前輸入完整的字節(jié)slice。定義一個(gè)和json.Decoder類似的sexpr.Decoder類型,支持從一個(gè)io.Reader流解碼。修改sexpr.Unmarshal函數(shù),使用這個(gè)新的類型實(shí)現(xiàn)。

練習(xí) 12.9: 編寫一個(gè)基于標(biāo)記的API用于解碼S表達(dá)式,參考xml.Decoder(7.14)的風(fēng)格。你將需要五種類型的標(biāo)記:Symbol、String、Int、StartList和EndList。

練習(xí) 12.10: 擴(kuò)展sexpr.Unmarshal函數(shù),支持布爾型、浮點(diǎn)數(shù)和interface類型的解碼,使用 練習(xí) 12.3: 的方案。(提示:要解碼接口,你需要將name映射到每個(gè)支持類型的reflect.Type。)



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)