12.7. 获取结构体字段标签

在4.5节我们使用构体成员标签用于设置对应JSON对应的名字。其中json成员标签让我们可以选择成员的名字和抑制零值成员的输出。在本节,我们将看到如何通过反射机制类获取成员标签。

对于一个web服务,大部分HTTP处理函数要做的第一件事情就是展开请求中的参数到本地变量中。我们定义了一个工具函数,叫params.Unpack,通过使用结构体成员标签机制来让HTTP处理函数解析请求参数更方便。

首先,我们看看如何使用它。下面的search函数是一个HTTP请求处理函数。它定义了一个匿名结构体类型的变量,用结构体的每个成员表示HTTP请求的参数。其中结构体成员标签指明了对于请求参数的名字,为了减少URL的长度这些参数名通常都是神秘的缩略词。Unpack将请求参数填充到合适的结构体成员中,这样我们可以方便地通过合适的类型类来访问这些参数。

gopl.io/ch12/search

  1. import "gopl.io/ch12/params"
  2. // search implements the /search URL endpoint.
  3. func search(resp http.ResponseWriter, req *http.Request) {
  4. var data struct {
  5. Labels []string `http:"l"`
  6. MaxResults int `http:"max"`
  7. Exact bool `http:"x"`
  8. }
  9. data.MaxResults = 10 // set default
  10. if err := params.Unpack(req, &data); err != nil {
  11. http.Error(resp, err.Error(), http.StatusBadRequest) // 400
  12. return
  13. }
  14. // ...rest of handler...
  15. fmt.Fprintf(resp, "Search: %+v\n", data)
  16. }

下面的Unpack函数主要完成三件事情。第一,它调用req.ParseForm()来解析HTTP请求。然后,req.Form将包含所有的请求参数,不管HTTP客户端使用的是GET还是POST请求方法。

下一步,Unpack函数将构建每个结构体成员有效参数名字到成员变量的映射。如果结构体成员有成员标签的话,有效参数名字可能和实际的成员名字不相同。reflect.Type的Field方法将返回一个reflect.StructField,里面含有每个成员的名字、类型和可选的成员标签等信息。其中成员标签信息对应reflect.StructTag类型的字符串,并且提供了Get方法用于解析和根据特定key提取的子串,例如这里的http:”…”形式的子串。

gopl.io/ch12/params

  1. // Unpack populates the fields of the struct pointed to by ptr
  2. // from the HTTP request parameters in req.
  3. func Unpack(req *http.Request, ptr interface{}) error {
  4. if err := req.ParseForm(); err != nil {
  5. return err
  6. }
  7. // Build map of fields keyed by effective name.
  8. fields := make(map[string]reflect.Value)
  9. v := reflect.ValueOf(ptr).Elem() // the struct variable
  10. for i := 0; i < v.NumField(); i++ {
  11. fieldInfo := v.Type().Field(i) // a reflect.StructField
  12. tag := fieldInfo.Tag // a reflect.StructTag
  13. name := tag.Get("http")
  14. if name == "" {
  15. name = strings.ToLower(fieldInfo.Name)
  16. }
  17. fields[name] = v.Field(i)
  18. }
  19. // Update struct field for each parameter in the request.
  20. for name, values := range req.Form {
  21. f := fields[name]
  22. if !f.IsValid() {
  23. continue // ignore unrecognized HTTP parameters
  24. }
  25. for _, value := range values {
  26. if f.Kind() == reflect.Slice {
  27. elem := reflect.New(f.Type().Elem()).Elem()
  28. if err := populate(elem, value); err != nil {
  29. return fmt.Errorf("%s: %v", name, err)
  30. }
  31. f.Set(reflect.Append(f, elem))
  32. } else {
  33. if err := populate(f, value); err != nil {
  34. return fmt.Errorf("%s: %v", name, err)
  35. }
  36. }
  37. }
  38. }
  39. return nil
  40. }

最后,Unpack遍历HTTP请求的name/valu参数键值对,并且根据更新相应的结构体成员。回想一下,同一个名字的参数可能出现多次。如果发生这种情况,并且对应的结构体成员是一个slice,那么就将所有的参数添加到slice中。其它情况,对应的成员值将被覆盖,只有最后一次出现的参数值才是起作用的。

populate函数小心用请求的字符串类型参数值来填充单一的成员v(或者是slice类型成员中的单一的元素)。目前,它仅支持字符串、有符号整数和布尔型。其中其它的类型将留做练习任务。

  1. func populate(v reflect.Value, value string) error {
  2. switch v.Kind() {
  3. case reflect.String:
  4. v.SetString(value)
  5. case reflect.Int:
  6. i, err := strconv.ParseInt(value, 10, 64)
  7. if err != nil {
  8. return err
  9. }
  10. v.SetInt(i)
  11. case reflect.Bool:
  12. b, err := strconv.ParseBool(value)
  13. if err != nil {
  14. return err
  15. }
  16. v.SetBool(b)
  17. default:
  18. return fmt.Errorf("unsupported kind %s", v.Type())
  19. }
  20. return nil
  21. }

如果我们上上面的处理程序添加到一个web服务器,则可以产生以下的会话:

  1. $ go build gopl.io/ch12/search
  2. $ ./search &
  3. $ ./fetch 'http://localhost:12345/search'
  4. Search: {Labels:[] MaxResults:10 Exact:false}
  5. $ ./fetch 'http://localhost:12345/search?l=golang&l=programming'
  6. Search: {Labels:[golang programming] MaxResults:10 Exact:false}
  7. $ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100'
  8. Search: {Labels:[golang programming] MaxResults:100 Exact:false}
  9. $ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming'
  10. Search: {Labels:[golang programming] MaxResults:10 Exact:true}
  11. $ ./fetch 'http://localhost:12345/search?q=hello&x=123'
  12. x: strconv.ParseBool: parsing "123": invalid syntax
  13. $ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
  14. 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:”…”字串。