In Section 4.5 we used struct field tags to modify the
JSON encoding of Go struct values. The json
field tag lets us
choose alternative field names and suppress the output
of empty fields.
In this section, we’ll see how to access field tags using reflection.
In a web server, the first thing most HTTP handler functions do is
extract the request parameters into local variables.
We’ll define a utility function, params.Unpack
,
that uses struct field tags to make writing HTTP handlers
(§7.7) more convenient.
First, we’ll show how it’s used.
The search
function below is an HTTP handler.
It defines a variable called data
of an anonymous struct type
whose fields correspond to the HTTP request parameters.
The struct’s field tags specify the parameter names, which are often
short and cryptic since space is precious in a URL.
The Unpack
function populates the struct from the request so
that the parameters can be accessed conveniently and with an
appropriate type.
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 ", data) }
The Unpack
function below does three things.
First, it calls req.ParseForm()
to parse the request.
Thereafter, req.Form
contains all the parameters, regardless of
whether the HTTP client used the GET or the POST request method.
Next, Unpack
builds a mapping from the effective name of
each field to the variable for that field. The effective name may
differ from the actual name if the field has a tag.
The Field
method of reflect.Type
returns a reflect.StructField
that provides information about the
type of each field such as its name, type, and optional tag.
The Tag
field is a reflect.StructTag
,
which is a string type that provides a Get
method to parse and extract the substring for a particular
key, such as http:"..."
in this case.
// 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 }
Finally, Unpack
iterates over the name/value pairs of the HTTP
parameters and updates the corresponding struct fields.
Recall that the same parameter name may appear more than once.
If this happens, and the field is a slice, then all the values of that
parameter are accumulated into the slice. Otherwise, the field is
repeatedly overwritten so that only the last value has any effect.
The populate
function takes care of setting a single field
v
(or a single element of a slice field) from a parameter
value.
For now, it supports only strings, signed integers, and booleans.
Supporting other types is left as an exercise.
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 }
If we add the server
handler to a web server, this might be a
typical session:
$ 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
Exercise 12.11:
Write the corresponding Pack
function.
Given a struct value, Pack
should return a URL incorporating
the parameter values from the struct.
Exercise 12.12:
Extend the field tag notation to express parameter validity requirements.
For example, a string might need to be a valid email
address or credit-card number, and an integer might need to
be a valid US ZIP code.
Modify Unpack
to check these requirements.
Exercise 12.13:
Modify the S-expression encoder (§12.4)
and decoder (§12.6) so that they honor
the sexpr:"..."
field tag in a similar manner to
encoding/json
(§4.5).
18.118.145.114