diff --git a/CONTRIBUTORS.html b/CONTRIBUTORS.html index e47f49a..65bb27f 100644 --- a/CONTRIBUTORS.html +++ b/CONTRIBUTORS.html @@ -8,7 +8,7 @@
CrazySssst
foreversmart <njutree@gmail.com>
Xargin <cao1988228@163.com>
TODO
+接下來,讓我們看看如何改善聚合數據類型的顯示。我們併不想完全剋隆一個fmt.Sprint函數,我們隻是像構建一個用於調式用的Display函數,給定一個聚合類型x,打印這個值對應的完整的結構,同時記録每個發現的每個元素的路徑。讓我們從一個例子開始。
+e, _ := eval.Parse("sqrt(A / pi)")
+Display("e", e)
+
+在上面的調用中,傳入Display函數的參數是在7.9節一個表達式求值函數返迴的語法樹。Display函數的輸出如下:
+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"
+
+在可能的情況下,你應該避免在一個包中暴露和反射相關的接口。我們將定義一個未導出的display函數用於遞歸處理工作,導出的是Display函數,它隻是display函數簡單的包裝以接受interface{}類型的參數:
+gopl.io/ch12/display
+
+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包來簡化我們的例子實現。
+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的可以。和往常一樣,遍歷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節的電影類型上演變來的:
+type Movie struct {
+ Title, Subtitle string
+ Year int
+ Color bool
+ Actor map[string]string
+ Oscars []string
+ Sequel *string
+}
+
+讓我們聲明一個該類型的變量,然後看看Display函數如何顯示它:
+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電影對應的中文名是《奇愛博士》):
+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
類型:
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))
調用的輸出如下,當然不同環境得到的結果可能有差異:
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"
+...
+
+觀察下面兩個例子的區别:
+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總是返迴一個值的具體類型,因爲它是從一個接口值提取的內容。
+在第二個例子中,Display函數調用的是reflect.ValueOf(&i),它返迴一個指向i的指針,對應Ptr類型。在switch的Ptr分支中,通過調用Elem來返迴這個值,返迴一個Value來表示i,對應Interface類型。一個間接獲得的Value,就像這一個,可能代表任意類型的值,包括接口類型。內部的display函數遞歸調用自身,這次它將打印接口的動態類型和值。
+目前的實現,Display如果顯示一個帶環的數據結構將會陷入死循環,例如首位項鏈的鏈表:
+// a struct that points to itself
+type Cycle struct{ Value int; Tail *Cycle }
+var c Cycle
+c = Cycle{42, &c}
+Display("c", c)
+
+Display會永遠不停地進行深度遞歸打印:
+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支持這類帶環的數據結構是比較棘手的,需要增加一個額外的記録訪問的路徑;代價是昂貴的。一般的解決方案是采用不安全的語言特性,我們將在13.3節看到具體的解決方案。
+帶環的數據結構很少會對fmt.Sprint函數造成問題,因爲它很少嚐試打印完整的數據結構。例如,當它遇到一個指針的時候,它隻是簡單第打印指針的數值。雖然,在打印包含自身的slice或map時可能遇到睏難,但是不保證處理這種是罕見情況卻可以避免額外的麻煩。
+練習 12.1: 擴展Displayhans,以便它可以顯示包含以結構體或數組作爲map的key類型的值。
+練習 12.2: 增強display函數的穩健性,通過記録邊界的步數來確保在超出一定限製前放棄遞歸。(在13.3節,我們會看到另一種探測數據結構是否存在環的技術。)
TODO
+Display是一個用於顯示結構化數據的調試工具,但是它併不能將任意的Go語言對象編碼爲通用消息然後用於進程間通信。
+正如我們在4.5節中中看到的,Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式,采用類似Lisp語言的語法。但是和其他編碼格式不同的是,Go語言自帶的標準庫併不支持S表達式,主要是因爲它沒有一個公認的標準規范。
+在本節中,我們將定義一個包用於將Go語言的對象編碼爲S表達式格式,它支持以下結構:
+42 integer
+"hello" string (with Go-style quotation)
+foo symbol (an unquoted name)
+(1 2 3) list (zero or more items enclosed in parentheses)
+
布爾型習慣上使用t符號表示true,空列表或nil符號表示false,但是爲了簡單起見,我們暫時忽略布爾類型。同時忽略的還有chan管道和函數,因爲通過反射併無法知道它們的確切狀態。我們忽略的還浮點數、複數和interface。支持它們是練習12.3的任務。
+我們將Go語言的類型編碼爲S表達式的方法如下。整數和字符串以自然的方式編碼。Nil值編碼爲nil符號。數組和slice被編碼爲一個列表。
+結構體被編碼爲成員對象的列表,每個成員對象對應一個個僅有兩個元素的子列表,其中子列表的第一個元素是成員的名字,子列表的第二個元素是成員的值。Map被編碼爲鍵值對的列表。傳統上,S表達式使用點狀符號列表(key . value)結構來表示key/value對,而不是用一個含雙元素的列表,不過爲了簡單我們忽略了點狀符號列表。
+編碼是由一個encode遞歸函數完成,如下所示。它的結構本質上和前面的Display函數類似:
+gopl.io/ch12/sexpr
+
+func encode(buf *bytes.Buffer, v reflect.Value) error {
+ switch v.Kind() {
+ case reflect.Invalid:
+ buf.WriteString("nil")
+
+ case reflect.Int, reflect.Int8, reflect.Int16,
+ reflect.Int32, reflect.Int64:
+ fmt.Fprintf(buf, "%d", v.Int())
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16,
+ reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ fmt.Fprintf(buf, "%d", v.Uint())
+
+ case reflect.String:
+ fmt.Fprintf(buf, "%q", v.String())
+
+ case reflect.Ptr:
+ return encode(buf, v.Elem())
+
+ case reflect.Array, reflect.Slice: // (value ...)
+ buf.WriteByte('(')
+ for i := 0; i < v.Len(); i++ {
+ if i > 0 {
+ buf.WriteByte(' ')
+ }
+ if err := encode(buf, v.Index(i)); err != nil {
+ return err
+ }
+ }
+ buf.WriteByte(')')
+
+ case reflect.Struct: // ((name value) ...)
+ buf.WriteByte('(')
+ for i := 0; i < v.NumField(); i++ {
+ if i > 0 {
+ buf.WriteByte(' ')
+ }
+ fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
+ if err := encode(buf, v.Field(i)); err != nil {
+ return err
+ }
+ buf.WriteByte(')')
+ }
+ buf.WriteByte(')')
+
+ case reflect.Map: // ((key value) ...)
+ buf.WriteByte('(')
+ for i, key := range v.MapKeys() {
+ if i > 0 {
+ buf.WriteByte(' ')
+ }
+ buf.WriteByte('(')
+ if err := encode(buf, key); err != nil {
+ return err
+ }
+ buf.WriteByte(' ')
+ if err := encode(buf, v.MapIndex(key)); err != nil {
+ return err
+ }
+ buf.WriteByte(')')
+ }
+ buf.WriteByte(')')
+
+ default: // float, complex, bool, chan, func, interface
+ return fmt.Errorf("unsupported type: %s", v.Type())
+ }
+ return nil
+}
+
+Marshal函數是對encode的保證,以保持和encoding/...下其它包有着相似的API:
+// Marshal encodes a Go value in S-expression form.
+func Marshal(v interface{}) ([]byte, error) {
+ var buf bytes.Buffer
+ if err := encode(&buf, reflect.ValueOf(v)); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+下面是Marshal對12.3節的strangelove變量編碼後的結果:
+((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
+ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
+ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
+ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
+"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
+("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
+omin.)" "Best Picture (Nomin.)")) (Sequel nil))
+
整個輸出編碼爲一行中以減少輸出的大小,但是也很難閲讀。這里有一個對S表達式格式化的約定。編寫一個S表達式的格式化函數將作爲一個具有挑戰性的練習任務;不過 http://gopl.io 也提供了一個簡單的版本。
+((Title "Dr. Strangelove")
+ (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
+ (Year 1964)
+ (Actor (("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")
+ ("Dr. Strangelove" "Peter Sellers")))
+ (Oscars ("Best Actor (Nomin.)"
+ "Best Adapted Screenplay (Nomin.)"
+ "Best Director (Nomin.)"
+ "Best Picture (Nomin.)"))
+ (Sequel nil))
+
和fmt.Print、json.Marshal、Display函數類似,sexpr.Marshal函數處理帶環的數據結構也會陷入死循環。
+在12.6節中,我們將給出S表達式解碼器的實現步驟,但是在那之前,我們還需要先了解如果通過反射技術來更新程序的變量。
+練習 12.3: 實現encode函數缺少的分支。將布爾類型編碼爲t和nil,浮點數編碼爲Go語言的格式,複數1+2i編碼爲#C(1.0 2.0)格式。接口編碼爲類型名和值對,例如("[]int" (1 2 3)),但是這個形式可能會造成歧義:reflect.Type.String方法對於不同的類型可能返迴相同的結果。
+練習 12.4: 脩改encode函數,以上面的格式化形式輸出S表達式。
+練習 12.5: 脩改encode函數,用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。
+練習 12.6: 脩改encode,作爲一個優化,忽略對是零值對象的編碼。
+練習 12.7: 創建一個基於流式的API,用於S表達式的解碼,和json.Decoder(§4.5)函數功能類似。
TODO
+到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。
+迴想一下,Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量,但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。
+對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的;其它一些則不可以。考慮以下的聲明語句:
+x := 2 // value type variable?
+a := reflect.ValueOf(2) // 2 int no
+b := reflect.ValueOf(x) // 2 int no
+c := reflect.ValueOf(&x) // &x *int no
+d := c.Elem() // 2 int yes (x)
+
+其中a對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址,它隻是一個指針&x
的拷貝。實際上,所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d,它是c的解引用方式生成的,指向另一個變量,因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem(),來獲取任意變量x對應的可取地址的Value。
我們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址:
+fmt.Println(a.CanAddr()) // "false"
+fmt.Println(b.CanAddr()) // "false"
+fmt.Println(c.CanAddr()) // "false"
+fmt.Println(d.CanAddr()) // "true"
+
+每當我們通過指針間接地獲取的reflect.Value都是可取地址的,卽使開始的是一個不可取地址的Value。在反射機製中,所有關於是否支持取地址的規則都是類似的。例如,slice的索引表達式e[i]將隱式地包含一個指針,它就是可取地址的,卽使開始的e表達式不支持也沒有關繫。以此類推,reflect.ValueOf(e).Index(i)對於的值也是可取地址的,卽使原始的reflect.ValueOf(e)不支持也沒有關繫。
+要從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法,它返迴一個Value,里面保存了指向變量的指針。然後是在Value上調用Interface()方法,也就是返迴一個interface{},里面通用包含指向變量的指針。最後,如果我們知道變量的類型,我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了:
+x := 2
+d := reflect.ValueOf(&x).Elem() // d refers to the variable x
+px := d.Addr().Interface().(*int) // px := &x
+*px = 3 // x = 3
+fmt.Println(x) // "3"
+
+或者,不使用指針,而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值:
+d.Set(reflect.ValueOf(4))
+fmt.Println(x) // "4"
+
+Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼,變量和值都是int類型,但是如果變量是int64類型,那麽程序將拋出一個panic異常,所以關鍵問題是要確保改類型的變量可以接受對應的值:
+d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
+
+通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常:
+x := 2
+b := reflect.ValueOf(x)
+b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
+
+這里有很多用於基本數據類型的Set方法:SetInt、SetUint、SetString和SetFloat等。
+d := reflect.ValueOf(&x).Elem()
+d.SetInt(3)
+fmt.Println(x) // "3"
+
+從某種程度上説,這些Set方法總是盡可能地完成任務。以SetInt爲例,隻要變量是某種類型的有符號整數就可以工作,卽使是一些命名的類型,隻要底層數據類型是有符號整數就可以,而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是:對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常,卽使那個interface{}變量對於整數類型也不行。
+x := 1
+rx := reflect.ValueOf(&x).Elem()
+rx.SetInt(2) // OK, x = 2
+rx.Set(reflect.ValueOf(3)) // OK, x = 3
+rx.SetString("hello") // panic: string is not assignable to int
+rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int
+
+var y interface{}
+ry := reflect.ValueOf(&y).Elem()
+ry.SetInt(2) // panic: SetInt called on interface Value
+ry.Set(reflect.ValueOf(3)) // OK, y = int(3)
+ry.SetString("hello") // panic: SetString called on interface Value
+ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
+
+當我們用Display顯示os.Stdout結構時,我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員,比如在類Unix繫統上os.File結構體中的fd int成員。然而,利用反射機製併不能脩改這些未導出的成員:
+stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
+fmt.Println(stdout.Type()) // "os.File"
+fd := stdout.FieldByName("fd")
+fmt.Println(fd.Int()) // "1"
+fd.SetInt(2) // panic: unexported field
+
+一個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員,如果是的話則拒絶脩改操作。因此,CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的:
+fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
+
TODO
+標準庫中encoding/...下每個包中提供的Marshal編碼函數都有一個對應的Unmarshal函數用於解碼。例如,我們在4.5節中看到的,要將包含JSON編碼格式的字節slice數據解碼爲我們自己的Movie類型(§12.3),我們可以這樣做:
+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方法返迴的記號。
+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。
+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。
+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對應的錯誤信息。
+// 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.Marshal一樣(譯註:這可能是筆誤,我覺得應該是指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。)
TODO
+在4.5節我們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓我們可以選擇成員的名字和抑製零值成員的輸出。在本節,我們將看到如果通過反射機製類獲取成員標籤。
+對於一個web服務,大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數,叫params.Unpack,通過使用結構體成員標籤機製來讓HTTP處理函數解析請求參數更方便。
+首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每個成員表示HTTP請求的參數。其中結構體成員標籤指明了對於請求參數的名字,爲了減少UTRL的長度這些參數名通常都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中,這樣我們可以方便地通過合適的類型類來訪問這些參數。
+gopl.io/ch12/search
+
+import "gopl.io/ch12/params"
+
+// search implements the /search URL endpoint.
+func search(resp http.ResponseWriter, req *http.Request) {
+ var data struct {
+ Labels []string `http:"l"`
+ MaxResults int `http:"max"`
+ Exact bool `http:"x"`
+ }
+ data.MaxResults = 10 // set default
+ if err := params.Unpack(req, &data); err != nil {
+ http.Error(resp, err.Error(), http.StatusBadRequest) // 400
+ return
+ }
+
+ // ...rest of handler...
+ fmt.Fprintf(resp, "Search: %+v\n", data)
+}
+
+下面的Unpack函數主要完成三件事情。第一,它調用req.ParseForm()來解析HTTP請求。然後,req.Form將包含所有的請求參數,不管HTTP客戶端使用的是GET還是POST請求方法。
+下一步,Unpack函數將構建每個結構體成員有效參數名字到成員變量的映射。如果結構體成員有成員標籤的話,有效參數名字可能和實際的成員名字不相同。reflect.Type的Field方法將返迴一個reflect.StructField,里面含有每個成員的名字、類型和可選的成員標籤等信息。其中成員標籤信息對應reflect.StructTag類型的字符串,併且提供了Get方法用於解析和根據特定key提取的子串,例如這里的http:"..."形式的子串。
+gopl.io/ch12/params
+
+// Unpack populates the fields of the struct pointed to by ptr
+// from the HTTP request parameters in req.
+func Unpack(req *http.Request, ptr interface{}) error {
+ if err := req.ParseForm(); err != nil {
+ return err
+ }
+
+ // Build map of fields keyed by effective name.
+ fields := make(map[string]reflect.Value)
+ v := reflect.ValueOf(ptr).Elem() // the struct variable
+ for i := 0; i < v.NumField(); i++ {
+ fieldInfo := v.Type().Field(i) // a reflect.StructField
+ tag := fieldInfo.Tag // a reflect.StructTag
+ name := tag.Get("http")
+ if name == "" {
+ name = strings.ToLower(fieldInfo.Name)
+ }
+ fields[name] = v.Field(i)
+ }
+
+ // Update struct field for each parameter in the request.
+ for name, values := range req.Form {
+ f := fields[name]
+ if !f.IsValid() {
+ continue // ignore unrecognized HTTP parameters
+ }
+ for _, value := range values {
+ if f.Kind() == reflect.Slice {
+ elem := reflect.New(f.Type().Elem()).Elem()
+ if err := populate(elem, value); err != nil {
+ return fmt.Errorf("%s: %v", name, err)
+ }
+ f.Set(reflect.Append(f, elem))
+ } else {
+ if err := populate(f, value); err != nil {
+ return fmt.Errorf("%s: %v", name, err)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+最後,Unpack遍歷HTTP請求的name/valu參數鍵值對,併且根據更新相應的結構體成員。迴想一下,同一個名字的參數可能出現多次。如果發生這種情況,併且對應的結構體成員是一個slice,那麽就將所有的參數添加到slice中。其它情況,對應的成員值將被覆蓋,隻有最後一次出現的參數值才是起作用的。
+populate函數小心用請求的字符串類型參數值來填充單一的成員v(或者是slice類型成員中的單一的元素)。目前,它僅支持字符串、有符號整數和布爾型。其中其它的類型將留做練習任務。
+func populate(v reflect.Value, value string) error {
+ switch v.Kind() {
+ case reflect.String:
+ v.SetString(value)
+
+ case reflect.Int:
+ i, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return err
+ }
+ v.SetInt(i)
+
+ case reflect.Bool:
+ b, err := strconv.ParseBool(value)
+ if err != nil {
+ return err
+ }
+ v.SetBool(b)
+
+ default:
+ return fmt.Errorf("unsupported kind %s", v.Type())
+ }
+ return nil
+}
+
+如果我們上上面的處理程序添加到一個web服務器,則可以産生以下的會話:
+$ go build gopl.io/ch12/search
+$ ./search &
+$ ./fetch 'http://localhost:12345/search'
+Search: {Labels:[] MaxResults:10 Exact:false}
+$ ./fetch 'http://localhost:12345/search?l=golang&l=programming'
+Search: {Labels:[golang programming] MaxResults:10 Exact:false}
+$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100'
+Search: {Labels:[golang programming] MaxResults:100 Exact:false}
+$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming'
+Search: {Labels:[golang programming] MaxResults:10 Exact:true}
+$ ./fetch 'http://localhost:12345/search?q=hello&x=123'
+x: strconv.ParseBool: parsing "123": invalid syntax
+$ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
+max: strconv.ParseInt: parsing "lots": invalid syntax
+
練習 12.11: 編寫相應的Pack函數,給定一個結構體值,Pack函數將返迴合併了所有結構體成員和值的URL。
+練習 12.12: 擴展成員標籤以表示一個請求參數的有效值規則。例如,一個字符串可以是有效的email地址或一個信用卡號碼,還有一個整數可能需要是有效的郵政編碼。脩改Unpack函數以檢査這些規則。
+練習 12.13: 脩改S表達式的編碼器(§12.4)和解碼器(§12.6),采用和encoding/json包(§4.5)類似的方式使用成員標籤中的sexpr:"..."字串。
TODO
+我們的最後一個例子是使用reflect.Type來打印任意值的類型和枚舉它的方法:
+gopl.io/ch12/methods
+
+// Print prints the method set of the value x.
+func Print(x interface{}) {
+ v := reflect.ValueOf(x)
+ t := v.Type()
+ fmt.Printf("type %s\n", t)
+
+ for i := 0; i < v.NumMethod(); i++ {
+ methType := v.Method(i).Type()
+ fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
+ strings.TrimPrefix(methType.String(), "func"))
+ }
+}
+
+reflect.Type和reflect.Value都提供了一個Method方法。每次t.Method(i)調用將一個reflect.Method的實例,對應一個用於描述一個方法的名稱和類型的結構體。每次v.Method(i)方法調用都返迴一個reflect.Value以表示對應的值(§6.4),也就是一個方法是幫到它的接收者的。使用reflect.Value.Call方法(我們之類沒有演示),將可以調用一個Func類型的Value,但是這個例子中隻用到了它的類型。
+這是屬於time.Duration和*strings.Replacer
兩個類型的方法:
methods.Print(time.Hour)
+// Output:
+// type time.Duration
+// func (time.Duration) Hours() float64
+// func (time.Duration) Minutes() float64
+// func (time.Duration) Nanoseconds() int64
+// func (time.Duration) Seconds() float64
+// func (time.Duration) String() string
+
+methods.Print(new(strings.Replacer))
+// Output:
+// type *strings.Replacer
+// func (*strings.Replacer) Replace(string) string
+// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
+`
+
TODO
+雖然反射提供的API遠多於我們講到的,我們前面的例子主要是給出了一個方向,通過反射可以實現哪些功能。反射是一個強大併富有表達力的工具,但是它應該被小心地使用,原因有三。
+第一個原因是,基於反射的代碼是比較脆弱的。對於每一個會導致編譯器報告類型錯誤的問題,在反射中都有與之相對應的問題,不同的是編譯器會在構建時馬上報告錯誤,而反射則是在眞正運行到的時候才會拋出panic異常,可能是寫完代碼很久之後的時候了,而且程序也可能運行了很長的時間。
+以前面的readList函數(§12.6)爲例,爲了從輸入讀取字符串併填充int類型的變量而調用的reflect.Value.SetString方法可能導致panic異常。絶大多數使用反射的程序都有類似的風險,需要非常小心地檢査每個reflect.Value的對於值的類型、是否可取地址,還有是否可以被脩改等。
+避免這種因反射而導致的脆弱性的問題的最好方法是將所有的反射相關的使用控製在包的內部,如果可能的話避免在包的API中直接暴露reflect.Value類型,這樣可以限製一些非法輸入。如果無法做到這一點,在每個有風險的操作前指向額外的類型檢査。以標準庫中的代碼爲例,當fmt.Printf收到一個非法的操作數是,它併不會拋出panic異常,而是打印相關的錯誤信息。程序雖然還有BUG,但是會更加容易診斷。
+fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"
+
+反射同樣降低了程序的安全性,還影響了自動化重構和分析工具的準確性,因爲它們無法識别運行時才能確認的類型信息。
+避免使用反射的第二個原因是,卽使對應類型提供了相同文檔,但是反射的操作不能做靜態類型檢査,而且大量反射的代碼通常難以理解。總是需要小心翼翼地爲每個導出的類型和其它接受interface{}或reflect.Value類型參數的函數維護説明文檔。
+第三個原因,基於反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對於一個典型的項目,大部分函數的性能和程序的整體性能關繫不大,所以使用反射可能會使程序更加清晰。測試是一個特别適合使用反射的場景,因爲每個測試的數據集都很小。但是對於性能關鍵路徑的函數,最好避免使用反射。
TODO
+函數聲明包括函數名、形式參數列表、返迴值列表(可省略)以及函數體。
+func name(parameter-list) (result-list) {
+ body
+}
+
形式參數列表描述了函數的參數名以及參數類型。這些參數作爲局部變量,其值由參數調用者提供。返迴值列表描述了函數返迴值的變量名以及類型。如果函數返迴一個無名變量或者沒有返迴值,返迴值列表的括號是可以省略的。如果一個函數聲明不包括返迴值列表,那麽函數體執行完畢後,不會返迴任何值。 +在hypot函數中,
+func hypot(x, y float64) float64 {
+ return math.Sqrt(x*x + y*y)
+}
+fmt.Println(hypot(3,4)) //"5"
+
x和y是形參名,3和4是調用時的傳入的實數,函數返迴了一個float64類型的值。 +返迴值也可以像形式參數一樣被命名。在這種情況下,每個返迴值被聲明成一個局部變量,併根據該返迴值的類型,將其初始化爲0。 +如果一個函數在聲明時,包含返迴值列表,該函數必鬚以 return語句結尾,除非函數明顯無法運行到結尾處。例如函數在結尾時調用了panic異常或函數中存在無限循環。
+正如hypot一樣,如果一組形參或返迴值有相同的類型,我們不必爲每個形參都寫出參數類型。下面2個聲明是等價的:
+func f(i, j, k int, s, t string){/* ...
+*/
+}
+func f(i int, j int, k int, s string, t string){/* ...
+*/
+}
+
下面,我們給出4種方法聲明擁有2個int型參數和1個int型返迴值的函數.blank identifier(譯者註:卽下文的_符號)可以強調某個參數未被使用。
+func add(x int, y int) int {return x + y}
+func sub(x, y int) (z int) { z = x - y; return}
+func first(x int, _ int) int { return x }
+func zero(int, int) int { return 0 }
+fmt.Printf("%T\n", add) //"func(int, int) int"
+fmt.Printf("%T\n", sub)//"func(int, int) int"
+fmt.Printf("%T\n", first)//"func(int, int) int"
+fmt.Printf("%T\n", zero)//"func(int, int) int"
+
函數的類型被稱爲函數的標識符。如果兩個函數形式參數列表和返迴值列表中的變量類型一一對應,那麽這兩個函數被認爲有相同的類型和標識符。形參和返迴值的變量名不影響函數標識符也不影響它們是否可以以省略參數類型的形式表示。
+每一次函數調用都必鬚按照聲明順序爲所有參數提供實參(參數值)。在函數調用時,Go語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返迴值的變量名對於函數調用者而言沒有意義。
+在函數體中,函數的形參作爲局部變量,被初始化爲調用者提供的值。函數的形參和有名返迴值作爲函數最外層的局部變量,被存儲在相同的詞法塊中。
+實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行脩改不會影響實參。但是,如果實參包括引用類型,如指針,slice(切片)、map、function、channel等類型,實參可能會由於函數的簡介引用被脩改。
+你可能會偶爾遇到沒有函數體的函數聲明,這表示該函數不是以Go實現的。這樣的聲明定義了函數標識符。
+package math
+
+func Sin(x float64) float //implemented in assembly language
+
TODO
+函數可以讓我們將一個語句序列打包爲一個單元,然後可以從程序中其它地方多次調用。函數的機製可以讓我們將一個大的工作分解爲小的任務,這樣的小任務可以讓不同程序員在不同時間、不同地方獨立完成。一個函數同時對用戶隱藏了其實現細節。由於這些因素,對於任何編程語言來説,函數都是一個至關重要的部分。
+我們已經見過許多函數了。現在,讓我們多花一點時間來徹底地討論函數特性。本章的運行示例是一個網絡蜘蛛,也就是web蒐索引擎中負責抓取網頁部分的組件,它們根據抓取網頁中的鏈接繼續抓取鏈接指向的頁面。一個網絡蜘蛛的例子給我們足夠的機會去探索遞歸函數、匿名函數、錯誤處理和函數其它的很多特性。