## 12.3. Display,一個遞歸的值打印器
接下來,讓我們看看如何改善聚合數據類型的顯示。我們并不想完全克隆一個fmt.Sprint函數,我們只是構建一個用于調試用的Display函數:給定任意一個復雜類型 x,打印這個值對應的完整結構,同時標記每個元素的發現路徑。讓我們從一個例子開始。
```Go
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
```
在上面的調用中,傳入Display函數的參數是在7.9節一個表達式求值函數返回的語法樹。Display函數的輸出如下:
```Go
Display e (eval.call):
e.fn = "sqrt"
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = "A"
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"
```
你應該盡量避免在一個包的API中暴露涉及反射的接口。我們將定義一個未導出的display函數用于遞歸處理工作,導出的是Display函數,它只是display函數簡單的包裝以接受interface{}類型的參數:
<u><i>gopl.io/ch12/display</i></u>
```Go
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
```
在display函數中,我們使用了前面定義的打印基礎類型——基本類型、函數和chan等——元素值的formatAtom函數,但是我們會使用reflect.Value的方法來遞歸顯示復雜類型的每一個成員。在遞歸下降過程中,path字符串,從最開始傳入的起始值(這里是“e”),將逐步增長來表示是如何達到當前值(例如“e.args[0].value”)的。
因為我們不再模擬fmt.Sprint函數,我們將直接使用fmt包來簡化我們的例子實現。
```Go
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalid\n", path)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
case reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]", path,
formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
```
讓我們針對不同類型分別討論。
**Slice和數組:** 兩種的處理邏輯是一樣的。Len方法返回slice或數組值中的元素個數,Index(i)獲得索引i對應的元素,返回的也是一個reflect.Value;如果索引i超出范圍的話將導致panic異常,這與數組或slice類型內建的len(a)和a[i]操作類似。display針對序列中的每個元素遞歸調用自身處理,我們通過在遞歸處理時向path附加“[i]”來表示訪問路徑。
雖然reflect.Value類型帶有很多方法,但是只有少數的方法能對任意值都安全調用。例如,Index方法只能對Slice、數組或字符串類型的值調用,如果對其它類型調用則會導致panic異常。
**結構體:** NumField方法報告結構體中成員的數量,Field(i)以reflect.Value類型返回第i個成員的值。成員列表也包括通過匿名字段提升上來的成員。為了在path添加“.f”來表示成員路徑,我們必須獲得結構體對應的reflect.Type類型信息,然后訪問結構體第i個成員的名字。
**Maps:** MapKeys方法返回一個reflect.Value類型的slice,每一個元素對應map的一個key。和往常一樣,遍歷map時順序是隨機的。MapIndex(key)返回map中key對應的value。我們向path添加“[key]”來表示訪問路徑。(我們這里有一個未完成的工作。其實map的key的類型并不局限于formatAtom能完美處理的類型;數組、結構體和接口都可以作為map的key。針對這種類型,完善key的顯示信息是練習12.1的任務。)
**指針:** Elem方法返回指針指向的變量,依然是reflect.Value類型。即使指針是nil,這個操作也是安全的,在這種情況下指針是Invalid類型,但是我們可以用IsNil方法來顯式地測試一個空指針,這樣我們可以打印更合適的信息。我們在path前面添加“*”,并用括弧包含以避免歧義。
**接口:** 再一次,我們使用IsNil方法來測試接口是否是nil,如果不是,我們可以調用v.Elem()來獲取接口對應的動態值,并且打印對應的類型和值。
現在我們的Display函數總算完工了,讓我們看看它的表現吧。下面的Movie類型是在4.5節的電影類型上演變來的:
```Go
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
```
讓我們聲明一個該類型的變量,然后看看Display函數如何顯示它:
```Go
strangelove := Movie{
Title: "Dr. Strangelove",
Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
Year: 1964,
Color: false,
Actor: map[string]string{
"Dr. Strangelove": "Peter Sellers",
"Grp. Capt. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
`Maj. T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Best Actor (Nomin.)",
"Best Adapted Screenplay (Nomin.)",
"Best Director (Nomin.)",
"Best Picture (Nomin.)",
},
}
```
Display("strangelove", strangelove)調用將顯示(strangelove電影對應的中文名是《奇愛博士》):
```Go
Display strangelove (display.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
strangelove.Oscars[0] = "Best Actor (Nomin.)"
strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
strangelove.Oscars[2] = "Best Director (Nomin.)"
strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil
```
我們也可以使用Display函數來顯示標準庫中類型的內部結構,例如`*os.File`類型:
```Go
Display("os.Stderr", os.Stderr)
// Output:
// Display os.Stderr (*os.File):
// (*(*os.Stderr).file).fd = 2
// (*(*os.Stderr).file).name = "/dev/stderr"
// (*(*os.Stderr).file).nepipe = 0
```
可以看出,反射能夠訪問到結構體中未導出的成員。需要當心的是這個例子的輸出在不同操作系統上可能是不同的,并且隨著標準庫的發展也可能導致結果不同。(這也是將這些成員定義為私有成員的原因之一!)我們甚至可以用Display函數來顯示reflect.Value 的內部構造(在這里設置為`*os.File`的類型描述體)。`Display("rV", reflect.ValueOf(os.Stderr))`調用的輸出如下,當然不同環境得到的結果可能有差異:
```Go
Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = "*os.File"
(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
...
```
觀察下面兩個例子的區別:
```Go
var i interface{} = 3
Display("i", i)
// Output:
// Display i (int):
// i = 3
Display("&i", &i)
// Output:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3
```
在第一個例子中,Display函數調用reflect.ValueOf(i),它返回一個Int類型的值。正如我們在12.2節中提到的,reflect.ValueOf總是返回一個具體類型的 Value,因為它是從一個接口值提取的內容。
在第二個例子中,Display函數調用的是reflect.ValueOf(&i),它返回一個指向i的指針,對應Ptr類型。在switch的Ptr分支中,對這個值調用 Elem 方法,返回一個Value來表示變量 i 本身,對應Interface類型。像這樣一個間接獲得的Value,可能代表任意類型的值,包括接口類型。display函數遞歸調用自身,這次它分別打印了這個接口的動態類型和值。
對于目前的實現,如果遇到對象圖中含有回環,Display將會陷入死循環,例如下面這個首尾相連的鏈表:
```Go
// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)
```
Display會永遠不停地進行深度遞歸打印:
```Go
Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
...ad infinitum...
```
許多Go語言程序都包含了一些循環的數據。讓Display支持這類帶環的數據結構需要些技巧,需要額外記錄迄今訪問的路徑;相應會帶來成本。通用的解決方案是采用 unsafe 的語言特性,我們將在13.3節看到具體的解決方案。
帶環的數據結構很少會對fmt.Sprint函數造成問題,因為它很少嘗試打印完整的數據結構。例如,當它遇到一個指針的時候,它只是簡單地打印指針的數字值。在打印包含自身的slice或map時可能卡住,但是這種情況很罕見,不值得付出為了處理回環所需的開銷。
**練習 12.1:** 擴展Display函數,使它可以顯示包含以結構體或數組作為map的key類型的值。
**練習 12.2:** 增強display函數的穩健性,通過記錄邊界的步數來確保在超出一定限制后放棄遞歸。(在13.3節,我們會看到另一種探測數據結構是否存在環的技術。)
- 前言
- 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讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言