<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 第十二章 反射 Go語音提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法和它們支持的內在操作,但是在編譯時并不知道這些變量的具體類型。這種機制被稱為反射。反射也可以讓我們將類型本身作為第一類的值類型處理。 在本章,我們將探討Go語言的反射特性,看看它可以給語言增加哪些表達力,以及在兩個至關重要的API是如何用反射機制的:一個是fmt包提供的字符串格式功能,另一個是類似encoding/json和encoding/xml提供的針對特定協議的編解碼功能。對于我們在4.6節中看到過的text/template和html/template包,它們的實現也是依賴反射技術的。然后,反射是一個復雜的內省技術,不應該隨意使用,因此,盡管上面這些包內部都是用反射技術實現的,但是它們自己的API都沒有公開反射相關的接口。 ### 12.1. 為何需要反射? 有時候我們需要編寫一個函數能夠處理一類并不滿足普通公共接口的類型的值,也可能是因為它們并沒有確定的表示方式,或者是在我們設計該函數的時候還這些類型可能還不存在,各種情況都有可能。 一個大家熟悉的例子是fmt.Fprintf函數提供的字符串格式化處理邏輯,它可以用例對任意類型的值格式化并打印,甚至支持用戶自定義的類型。讓我們也來嘗試實現一個類似功能的函數。為了簡單起見,我們的函數只接收一個參數,然后返回和fmt.Sprint類似的格式化后的字符串。我們實現的函數名也叫Sprint。 我們使用了switch類型分支首先來測試輸入參數是否實現了String方法,如果是的話就使用該方法。然后繼續增加類型測試分支,檢查是否是每個基于string、int、bool等基礎類型的動態類型,并在每種情況下執行相應的格式化操作。 ~~~ func Sprint(x interface{}) string { type stringer interface { String() string } switch x := x.(type) { case stringer: return x.String() case string: return x case int: return strconv.Itoa(x) // ...similar cases for int16, uint32, and so on... case bool: if x { return "true" } return "false" default: // array, chan, func, map, pointer, slice, struct return "???" } } ~~~ 但是我們如何處理其它類似[]float64、map[string][]string等類型呢?我們當然可以添加更多的測試分支,但是這些組合類型的數目基本是無窮的。還有如何處理url.Values等命名的類型呢?雖然類型分支可以識別出底層的基礎類型是map[string][]string,但是它并不匹配url.Values類型,因為它們是兩種不同的類型,而且switch類型分支也不可能包含每個類似url.Values的類型,這會導致對這些庫的循環依賴。 沒有一種方法來檢查未知類型的表示方式,我們被卡住了。這就是我們為何需要反射的原因。 ### 12.2. reflect.Type和reflect.Value 反射是由 reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個Go類型. 它是一個接口, 有許多方法來區分類型和檢查它們的組件, 例如一個結構體的成員或一個函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 同樣的實體標識了動態類型的接口值. 函數 reflect.TypeOf 接受任意的 interface{} 類型, 并返回對應動態類型的reflect.Type: ~~~ t := reflect.TypeOf(3) // a reflect.Type fmt.Println(t.String()) // "int" fmt.Println(t) // "int" ~~~ 其中 TypeOf(3) 調用將值 3 作為 interface{} 類型參數傳入. 回到 7.5節 的將一個具體的值轉為接口類型會有一個隱式的接口轉換操作, 它會創建一個包含兩個信息的接口值: 操作數的動態類型(這里是int)和它的動態的值(這里是3). 因為 reflect.TypeOf 返回的是一個動態類型的接口值, 它總是返回具體的類型. 因此, 下面的代碼將打印 "*os.File" 而不是 "io.Writer". 稍后, 我們將看到 reflect.Type 是具有識別接口類型的表達方式功能的. ~~~ var w io.Writer = os.Stdout fmt.Println(reflect.TypeOf(w)) // "*os.File" ~~~ 要注意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因為打印動態類型值對于調試和日志是有幫助的, fmt.Printf 提供了一個簡短的 %T 標志參數, 內部使用 reflect.TypeOf 的結果輸出: ~~~ fmt.Printf("%T\n", 3) // "int" ~~~ reflect 包中另一個重要的類型是 Value. 一個 reflect.Value 可以持有一個任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 并返回對應動態類型的reflect.Value. 和 reflect.TypeOf 類似, reflect.ValueOf 返回的結果也是對于具體的類型, 但是 reflect.Value 也可以持有一個接口值. ~~~ v := reflect.ValueOf(3) // a reflect.Value fmt.Println(v) // "3" fmt.Printf("%v\n", v) // "3" fmt.Println(v.String()) // NOTE: "<int Value>" ~~~ 和 reflect.Type 類似, reflect.Value 也滿足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否則 String 只是返回具體的類型. 相同, 使用 fmt 包的 %v 標志參數, 將使用 reflect.Values 的結果格式化. 調用 Value 的 Type 方法將返回具體類型所對應的 reflect.Type: ~~~ t := v.Type() // a reflect.Type fmt.Println(t.String()) // "int" ~~~ 逆操作是調用 reflect.ValueOf 對應的 reflect.Value.Interface 方法. 它返回一個 interface{} 類型表示 reflect.Value 對應類型的具體值: ~~~ v := reflect.ValueOf(3) // a reflect.Value x := v.Interface() // an interface{} i := x.(int) // an int fmt.Printf("%d\n", i) // "3" ~~~ 一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此只有我們知道具體的動態類型才能使用類型斷言來訪問內部的值(就像上面那樣), 對于內部值并沒有特別可做的事情. 相比之下, 一個 Value 則有很多方法來檢查其內容, 無論它的具體類型是什么. 讓我們再次嘗試實現我們的格式化函數 format.Any. 我們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.) *gopl.io/ch12/format* ~~~ package format import ( "reflect" "strconv" ) // Any formats any value as a string. func Any(value interface{}) string { return formatAtom(reflect.ValueOf(value)) } // formatAtom formats a value without inspecting its internal structure. func formatAtom(v reflect.Value) string { switch v.Kind() { case reflect.Invalid: return "invalid" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(v.Uint(), 10) // ...floating-point and complex cases omitted for brevity... case reflect.Bool: return strconv.FormatBool(v.Bool()) case reflect.String: return strconv.Quote(v.String()) case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: return v.Type().String() + " 0x" + strconv.FormatUint(uint64(v.Pointer()), 16) default: // reflect.Array, reflect.Struct, reflect.Interface return v.Type().String() + " value" } } ~~~ 到目前為止, 我們的函數將每個值視作一個不可分割沒有內部結構的, 因此它叫 formatAtom. 對于聚合類型(結構體和數組)個接口只是打印類型的值, 對于引用類型(channels, functions, pointers, slices, 和 maps), 它十六進制打印類型的引用地址. 雖然還不夠理想, 但是依然是一個重大的進步, 并且 Kind 只關心底層表示, format.Any 也支持新命名的類型. 例如: ~~~ var x int64 = 1 var d time.Duration = 1 * time.Nanosecond fmt.Println(format.Any(x)) // "1" fmt.Println(format.Any(d)) // "1" fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0" fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0" ~~~ ### 12.3. Display遞歸打印 接下來,讓我們看看如何改善聚合數據類型的顯示。我們并不想完全克隆一個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節,我們會看到另一種探測數據結構是否存在環的技術。) ### 12.4. 示例: 編碼S表達式 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)函數功能類似。 ### 12.5. 通過reflect.Value修改值 到目前為止,反射還只是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機制來修改變量。 回想一下,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" ~~~ ### 12.6. 示例: 解碼S表達式 標準庫中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.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。) ### 12.7. 獲取結構體字段標識 在4.5節我們使用構體成員標簽用于設置對應JSON對應的名字。其中json成員標簽讓我們可以選擇成員的名字和抑制零值成員的輸出。在本節,我們將看到如果通過反射機制類獲取成員標簽。 對于一個web服務,大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數,叫params.Unpack,通過使用結構體成員標簽機制來讓HTTP處理函數解析請求參數更方便。 首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每個成員表示HTTP請求的參數。其中結構體成員標簽指明了對于請求參數的名字,為了減少URL的長度這些參數名通常都是神秘的縮略詞。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:"..."字串。 ### 12.8. 顯示一個類型的方法集 我們的最后一個例子是使用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) ~~~ ### 12.9. 幾點忠告 雖然反射提供的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類型參數的函數維護說明文檔。 第三個原因,基于反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對于一個典型的項目,大部分函數的性能和程序的整體性能關系不大,所以使用反射可能會使程序更加清晰。測試是一個特別適合使用反射的場景,因為每個測試的數據集都很小。但是對于性能關鍵路徑的函數,最好避免使用反射。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看