Go語言獲取結構體欄位標識

2020-07-16 10:04:52
在本節,我們將看到如何通過反射機制類獲取成員標籤。

對於一個 web 服務,大部分 HTTP 處理常式要做的第一件事情就是展開請求中的引數到本地變數中。我們定義了一個工具函數,叫 params.Unpack,通過使用結構體成員標籤機制來讓 HTTP 處理常式解析請求引數更方便。

首先,我們看看如何使用它。下面的 search 函數是一個 HTTP 請求處理常式。它定義了一個匿名結構體型別的變數,用結構體的每個成員表示 HTTP 請求的引數。

其中結構體成員標籤指明了對於請求引數的名字,為了減少 URL 的長度這些引數名通常都是神祕的縮略詞。Unpack 將請求引數填充到合適的結構體成員中,這樣我們可以方便地通過合適的型別類來存取這些引數。
import "gopl.io/ch12/params"
// 搜尋實現/search url端點。
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
    }
    // ...處理程式的其餘部分...
    fmt.Fprintf(resp, "Search: %+vn", data)
}
下面的 Unpack 函數主要完成三件事情。第一,它呼叫 req.ParseForm() 來解析 HTTP 請求。然後,req.Form 將包含所有的請求引數,不管 HTTP 用戶端使用的是 GET 還是 POST 請求方法。

下一步,Unpack 函數將構建每個結構體成員有效引數名字到成員變數的對映。如果結構體成員有成員標籤的話,有效引數名字可能和實際的成員名字不相同。reflect.Type 的 Field 方法將返回一個 reflect.StructField,裡面含有每個成員的名字、型別和可選的成員標籤等資訊。

其中成員標籤資訊對應 reflect.StructTag 型別的字串,並且提供了 Get 方法用於解析和根據特定 key 提取的子串,例如這裡的 http:"..." 形式的子串。
// unpack填充ptr指向的結構的欄位
// 從請求中的HTTP請求引數。
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }
    // 構建由有效名稱鍵控的欄位的對映。
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem() // 結構變數
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i) // 反射.StructField
        tag := fieldInfo.Tag // 一個 reflect.StructTag
        name := tag.Get("http")
        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
}
最後,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