## 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函數處理。對於map,key可能是任意類型,對元素的處理方式和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.Decoder(7.14)的風格。你將需要五種類型的標記:Symbol、String、Int、StartList和EndList。
**練習 12.10:** 擴展sexpr.Unmarshal函數,支持布爾型、浮點數和interface類型的解碼,使用 **練習 12.3:** 的方案。(提示:要解碼接口,你需要將name映射到每個支持類型的reflect.Type。)
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 査找重複的行
- GIF動畵
- 獲取URL
- 併發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 複數
- 布爾型
- 字符串
- 常量
- 複合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模闆
- 函數
- 函數聲明
- 遞歸
- 多返迴值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基於指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基於類型斷言識别錯誤類型
- 通過類型斷言査詢接口
- 類型分支
- 示例: 基於標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 併發的Clock服務
- 示例: 併發的Echo服務
- Channels
- 併發的循環
- 示例: 併發的Web爬蟲
- 基於select的多路複用
- 示例: 併發的字典遍歷
- 併發的退出
- 示例: 聊天服務
- 基於共享變量的併發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- sync.Once初始化
- 競爭條件檢測
- 示例: 併發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 爲何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value脩改值
- 示例: 解碼S表達式
- 獲取結構體字段標識
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附録