From 4831e0098978bac9d0c3185fd7045f988732f9b8 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Sat, 2 Jan 2016 15:55:52 +0800 Subject: [PATCH] ch12-07 done Fixes #116 --- ch12/ch12-07.md | 139 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/ch12/ch12-07.md b/ch12/ch12-07.md index bdfdcff..ed3503a 100644 --- a/ch12/ch12-07.md +++ b/ch12/ch12-07.md @@ -1,3 +1,140 @@ ## 12.7. 獲取結構體字段標識 -TODO +在4.5節我們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓我們可以選擇成員的名字和抑製零值成員的輸出。在本節,我們將看到如果通過反射機製類獲取成員標籤。 + +對於一個web服務,大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數,叫params.Unpack,通過使用結構體成員標籤機製來讓HTTP處理函數解析請求參數更方便。 + +首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每個成員表示HTTP請求的參數。其中結構體成員標籤指明了對於請求參數的名字,爲了減少UTRL的長度這些參數名通常都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中,這樣我們可以方便地通過合適的類型類來訪問這些參數。 + +```Go +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:"..."形式的子串。 + +```Go +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類型成員中的單一的元素)。目前,它僅支持字符串、有符號整數和布爾型。其中其它的類型將留做練習任務。 + +```Go +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:"..."字串。 + + +