gopl-zh.github.com/ch12/ch12-06.md

163 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 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方法返迴的記號。
```Go
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。
```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函數處理。對於mapkey可能是任意類型對元素的處理方式和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.Decoder7.14的風格。你將需要五種類型的標記Symbol、String、Int、StartList和EndList。
**練習 12.10** 擴展sexpr.Unmarshal函數支持布爾型、浮點數和interface類型的解碼使用 **練習 12.3** 的方案。提示要解碼接口你需要將name映射到每個支持類型的reflect.Type。