本篇各章節的主要內容: golang
到目前爲止,反射只是用來解析變量值。本節的重點是改變值。 json
reflect.Value 的值,有些是可尋址的,有些是不可尋址的。經過 reflect.ValueOf(x) 返回的 reflect.Value 都是不可尋址的。可是經過指針提領得來的 reflect.Value 是可尋址的。能夠經過調用 reflect.ValueOf(&x).Elem() 來得到任意變量 x 可尋址的 reflect.Value 值。
能夠經過變量的 CanAddr 方法來詢問 reflect.Value 變量是否可尋址:數組
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) fmt.Println(a.CanAddr()) // "false" fmt.Println(b.CanAddr()) // "false" fmt.Println(c.CanAddr()) // "false" fmt.Println(d.CanAddr()) // "true"
從一個可尋址的 reflect.Value() 獲取變量須要三步:瀏覽器
以後,就能夠經過這個指針來更新變量了:安全
x := 2 d := reflect.ValueOf(&x).Elem() // d表明變量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"
若是類型不匹配會致使程序崩潰:數據結構
d.Set(reflect.ValueOf(int64(5))) // panic: int64 不可賦值給 int
在一個不可尋址的 reflect.Value 上調用 Set 方法也會使程序崩潰:ide
x := 2 b := reflect.ValueOf(x) b.Set(reflect.ValueOf(3)) // panic: 在不可尋址的值上使用 Set 方法
另外還提供了一些爲基本類型特化的 Set 變種:SetInt、SetUint、SetString、SetFloat等:函數
d := reflect.ValueOf(&x).Elem() d.SetInt(3) fmt.Println(x) // "3"
這些方法還有必定的容錯性。好比 SetInt 方法,任意有符號整型,甚至是底層類型是有符號整型的命名類型,均可以執行成功。若是值太大了,會無提示地截斷它。可是在指向 interface{} 變量的 reflect.Value 上調用 SetInt 會崩潰(儘管使用 Set 是沒有問題的):工具
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 不能賦值給 int rx.Set(reflect.ValueOf("hello")) // panic: string 不能賦值給 int var y interface{} ry := reflect.ValueOf(&y).Elem() ry.SetInt(2) // panic: 在指向空接口的 Value 上調用 SetInt ry.Set(reflect.ValueOf(3)) // OK, y = int(3) ry.SetString("hello") // panic: 在指向空接口的 Value 上調用 SetString ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
另外,反射能夠越過 Go 言語的導出規則,讀取到未導出的成員。可是利用反射不能修改未導出的成員:
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, 一個 os.File 變量 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"
本節要爲 S 表達式編碼實現一個簡單的 Unmarshal 函數(解碼器)。一個健壯的和通用的實現比這裏的例子須要更多的代碼,這裏精簡了不少,只支持 S 表達式有限的子集,而且沒有優雅地處理錯誤。代碼的目的是闡釋反射,而不是語法分析。
詞法分析器 lexer 使用 text\/scanner 包提供的掃描器 Scanner 類型來把輸入流分解成一系列的標記(token),包括註釋、標識符、字符串字面量和數字字面量。掃描器的 Scan 方法將提早掃描並返回下一個標記(類型爲 rune)。大部分標記(好比'(')都只包含單個rune,但 text\/scanner 包也能夠支持由多個字符組成的記號。調用 Scan 會返回標記的類型,調用 TokenText 則會返回標記的文本。
由於每一個解析器可能須要屢次使用當前的記號,可是 Scan 會一直向前掃描,因此把掃描器封裝到一個 lexer 輔助類型中,其中保存了 Scan 最近返回的標記:
type lexer struct { scan scanner.Scanner token rune // 當前標記 } 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 { // 注意: 錯誤處理不是這篇的重點,簡單粗暴的處理了 panic(fmt.Sprintf("got %q, want %q", lex.text(), want)) } lex.next() }
分析器有兩個主要的函數。
一個是read,它讀取從當前標記開始的 S 表達式,並更新由可尋址的 reflect.Value 類型的變量 v 指向的變量:
func read(lex *lexer, v reflect.Value) { switch lex.token { case scanner.Ident: // 僅有的有標識符是 「nil」 和結構體的字段名 if lex.text() == "nil" { v.Set(reflect.Zero(v.Type())) lex.next() return } case scanner.String: s, _ := strconv.Unquote(lex.text()) // 注意:錯誤被忽略 v.SetString(s) lex.next() return case scanner.Int: i, _ := strconv.Atoi(lex.text()) // 注意:錯誤被忽略 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 函數只處理後一種狀況。當它遇到 scanner.Ident 的值爲 「nil」 時,經過 reflect.Zero 函數把 v 設置爲其類型的零值。對於其餘標識符,則應該產生一個錯誤(這裏則是採用簡單粗暴的方法,直接忽略了)。
還有一個是 readList 函數。一個 '(' 標記表明一個列表的開始,readList 函數可把列表解碼爲多種類型:map、結構體、切片或者數組,具體類型根據傳入待填充變量的類型決定。對於每種類型都會循環解析內容直到遇到匹配的右括號 ')',這個是用 endList 函數來檢測的。
比較有趣的地方是遞歸。最簡單的例子是處理數組,在遇到 ')' 以前,使用 Index 方法來得到數組的一個元素,再遞歸調用 read 來填充數據。切片的流程與數組相似,不一樣之處是先建立每個元素變量,再填充,最後追加到切片中。
結構體和map在循環的每一輪中都必須解析一個關於(key value)的子列表。對於結構體,key 是用來定位字段的符號。與數組相似,經過 FieldByName 函數來得到結構體對應字段的變量,再遞歸調用 read 來填充。對於 map,key 能夠是任何類型。與切片相似,先建立新變量,再遞歸地填充,最後再把新的鍵值對添加到 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,隱藏了實現中多個不完美的地方,好比解析過程當中遇到錯誤會崩潰,所以使用了一個延遲調用來從崩潰中恢復,而且返回錯誤消息:
// Unmarshal 解析 S 表達式數據而且填充到非 nil 指針 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() // 獲取第一個標記 defer func() { // 注意: 錯誤處理不是這篇的重點,簡單粗暴的處理了 if x := recover(); x != nil { err = fmt.Errorf("error at %s: %v", lex.scan.Position, x) } }() read(lex, reflect.ValueOf(out).Elem()) return nil }
一個具有用於生產環境的質量的實現對任何的輸入都不該當崩潰,並且應當對每次錯誤詳細報告信息,可能的話,應當包含行號或者偏移量。經過這個示例有助於瞭解 encoding/json 這類包的底層機制,以及如何使用反射來填充數據結構。
這裏的「成員」和「字段」兩個詞有點混用,但都是同一個意思。
可使用結構體成員標籤(field tag)在進行JSON反序列化的時候對應JSON中字段的名字。json 成員標籤讓咱們能夠選擇其餘的字段名以及忽略輸出的空字段。這小節將經過反射機制獲取結構體字段的標籤,而後填充字段的值,就和JSON反序列化同樣,目標和結果是同樣的,只是獲取的數據源不一樣。
有一個 Web 服務應用的場景,在 Web 服務器中,絕大部分 HTTP 處理函數的第一件事就是提取請求參數到局部變量中。這裏將定義一個工具函數 params.Unpack,使用結構體成員標籤直接將參數填充到結構體對應的字段中。由於 URL 的長度有限,因此參數的名稱通常比較短,含義也比較模糊。這須要經過成員標籤將結構體的字段和參數名稱對應上。
首先,展現這個工具函數的用法。就是假設已經實現了這個 params.Unpack 函數,下面的 search 函數就是一個 HTTP 處理函數,它定義了一個結構體變量 data,data 也定義了成員標籤來對應請求參數的名字。Unpack 函數從請求中提取數據來填充這個結構體,這樣不只能夠更方便的訪問,還避免了手動轉換類型:
package main import ( "fmt" "net/http" ) import "gopl/ch12/params" // search 用於處理 /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 // 設置默認值 if err := params.Unpack(req, &data); err != nil { http.Error(resp, err.Error(), http.StatusBadRequest) // 400 return } // ...其餘處理代碼... fmt.Fprintf(resp, "Search: %+v\n", data) } // 這裏還缺乏一個 main 函數,最後會補上
下面的 Unpack 函數作了三件事情:
1、調用 req.ParseForm() 來解析請求。在這以後,req.Form 就有了全部的請求參數,這個方法對 HTTP 的 GET 和 POST 請求都適用。
2、Unpack 函數構造了一個從每一個有效字段名到對應字段變量的映射。在字段有標籤時,有效字段名與實際字段名能夠不一樣。reflect.Type 的 Field 方法會返回一個 reflect.StructField 類型,這個類型提供了每一個字段的名稱、類型以及一個可選的標籤。它的 Tag 字段類型爲 reflect.StructTag,底層類型爲字符串,提供了一個 Get 方法用於解析和提取對於一個特定 key 的子串,好比上面示例中結構體字段後面的 http:"max"
這種形式的字段標籤。
3、Unpack 遍歷 HTTP 參數中的全部 key\/value 對,而且更新對應的結構體字段。同一個參數能夠出現屢次。若是對應的字段是切片,則參數全部的值都會追加到切片裏。不然,這個字段會被屢次覆蓋,只有最後一次的值纔有效。
Unpack 函數的代碼以下:
// Unpack 從 HTTP 請求 req 的參數中提取數據填充到 ptr 指向的結構體的各個字段 func Unpack(req *http.Request, ptr interface{}) error { if err := req.ParseForm(); err != nil { return err } // 建立字段映射表,key 爲有效名稱 fields := make(map[string]reflect.Value) v := reflect.ValueOf(ptr).Elem() // reflect.ValueOf(&x).Elem() 得到任意變量 x 可尋址的值,用於設置值。 for i := 0; i < v.NumField(); i++ { fieldInfo := v.Type().Field(i) // a reflect.StructField,提供了每一個字段的名稱、類型以及一個可選的標籤 tag := fieldInfo.Tag // a reflect.Structtag,底層類型爲字符串,提供了一個 Get 方法,下一行就用到了 name := tag.Get("http") // Get 方法用於解析和提取對於一個特定 key 的子串 if name == "" { name = strings.ToLower(fieldInfo.Name) } fields[name] = v.Field(i) } // 對請求中的每一個參數更新結構體中對應的字段 for name, values := range req.Form { f := fields[name] if !f.IsValid() { continue // 忽略不能識別的 HTTP 參數 } 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 }
這裏還調用了一個 populate 函數,負責從單個 HTTP 請求參數值填充單個字段 v (或者切片字段中的單個元素)。目前,它僅支持字符串、有符號整數和布爾值。要支持其餘類型能夠再添加:
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 }
接着把 search 處理程序添加到一個 Web 服務器中,直接在 search 所在的 main 包的命令源碼文件中添加下面的 main 函數:
func main() { fmt.Println("http://localhost:8000/search") // Search: {Labels:[] MaxResults:10 Exact:false} fmt.Println("http://localhost:8000/search?l=golang&l=gopl") // Search: {Labels:[golang gopl] MaxResults:10 Exact:false} fmt.Println("http://localhost:8000/search?l=gopl&x=1") // Search: {Labels:[gopl] MaxResults:10 Exact:true} fmt.Println("http://localhost:8000/search?x=true&max=100&max=200&l=golang") // Search: {Labels:[golang] MaxResults:200 Exact:true} fmt.Println("http://localhost:8000/search?q=hello") // Search: {Labels:[] MaxResults:10 Exact:false} # 不存在的參數會忽略 fmt.Println("http://localhost:8000/search?x=123") // x: strconv.ParseBool: parsing "123": invalid syntax # x 提供的參數解析錯誤 fmt.Println("http://localhost:8000/search?max=lots") // max: strconv.ParseInt: parsing "lots": invalid syntax # max 提供的參數解析錯誤 http.HandleFunc("/search", search) log.Fatal(http.ListenAndServe(":8000", nil)) }
這裏提供了幾個示例以及輸出的結果,直接使用瀏覽器,輸入URL就能返回對應的結果。
經過反射的 reflect.Type 來獲取一個任意值的類型並枚舉它的方法。下面的例子是把類型和方法都打印出來:
package methods import ( "fmt" "reflect" "strings" ) // Print 輸出值 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 的方法:
下面是兩個示例測試,展現以及驗證上面的函數:
package methods_test import ( "strings" "time" "gopl/ch12/methods" ) func ExamplePrintDuration() { 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) Round(time.Duration) time.Duration // func (time.Duration) Seconds() float64 // func (time.Duration) String() string // func (time.Duration) Truncate(time.Duration) time.Duration } func ExamplePrintReplacer() { methods.Print(new(strings.Replacer)) // Output: // type *strings.Replacer // func (*strings.Replacer) Replace(string) string // func (*strings.Replacer) WriteString(io.Writer, string) (int, error) }
另外還有一個 reflect.Value.Call 方法,能夠調用 Func 類型的 Value,這裏沒有演示。
還有不少反射API,這裏的示例展現了反射能作哪些事情。
反射是一個功能和表達能力都很強大的工具,可是要慎用,主要有三個緣由。
基於反射的代碼是很脆弱的。通常編譯器在編譯時就能報告錯誤,可是反射錯誤則要等到執行時纔會以崩潰的方式來報告。這多是等待程序運行好久之後纔會發生。
好比,嘗試讀取一個字符串而後填充一個 Int 類型的變量,那麼調用 reflect.Value.SetString 就會崩潰。不少使用反射的程序都會有相似的風險。因此對每個 reflect.Value 都須要仔細檢查它的類型、是否可尋址、是否可設置。
要回避這種缺陷的最好的辦法就是確保反射的使用完整的封裝在包裏,而且若是可能,在包的 API 中避免使用 reflect.Value,儘可能使用特定的類型來確保輸入是合法的值。若是作不到,那就須要在每一個危險的操做前都作額外的動態檢查。好比標準庫的 fmt.Printf 能夠做爲一個示例,當遇到操做數類型不合適時,它不會崩潰,而是輸出一條描述性的錯誤消息。這儘管仍然會有 bug,但定位起來就簡單多了:
fmt.Printf("%d %s\n", "hello", 123) // %!d(string=hello) %!s(int=123)
反射還下降了自動重構和分析工具的安全性與準確度,由於它們沒法檢測到類型的信息。
類型也算是某種形式的文檔,而反射的相關操做則沒法作靜態類型檢查,因此大量使用反射的代碼是很難理解的。對應接收 interface{} 或者reflect.Value 的函數,必定要寫清楚指望的參數類型和其餘限制條件。
基於反射的函數會比爲特定類型優化的函數慢一到兩個數量級。在一個典型的程序中,大部分函數與總體性能無關,因此爲了讓程序更清晰可使用反射。好比測試就和適合使用反射,由於大部分測試都使用小數據集。但對性能關鍵路徑上的函數,最好避免使用反射。