W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
原文鏈接:https://gopl-zh.github.io/ch12/ch12-06.html
標(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。)
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: