Anyone who's written a website or webservice in Go is probably familiar with the http.Handler
interface and its ServeHTTP(res http.ResponseWriter, req *http.Request)
function. Many of the 3rd party web frameworks expose something similar, often with a custom Request
parameter.
Personally, I've never been a fan of having the response passed into actions like that. I understand why the standard library took this approach: some scenarios require the consumer to have access and control over the response. In most cases though, I find it more natural to think of the response as the action's return value.
To achieve this, the first thing I do is create a Response
interface and a simple implementation:
var ( NotFound = Empty(404) ServerError = Empty(500) ) type Response interface { WriteTo(out http.ResponseWriter) } type NormalResponse struct { status int body []byte header http.Header } func (r *NormalResponse) WriteTo(out http.ResponseWriter) { header := out.Header() for k, v := range r.header { header[k] = v } out.WriteHeader(r.status) out.Write(r.body) } func (r *NormalResponse) Cache(ttl string) *NormalResponse { return r.Header("Cache-Control", "public,max-age=" + ttl) } func (r *NormalResponse) Header(key, value string) *NormalResponse { r.header.Set(key, value) return r } // functions to create responses func Empty(status int) *NormalResponse { return Respond(status, nil) } func Json(status int, body interface{}) *NormalResponse { return Respond(status, body).Header("Content-Type", "application/json") } func Error(message string, err error) *NormalResponse { log.Println(message, err) return ServerError } func Respond(status int, body interface{}) *NormalResponse { var b []byte var err error switch t := body.(type) { case []byte: b = t case string: b = []byte(t) default: if b, err = json.Marshal(body); err != nil { return Error("body json marshal", err) } } return &NormalResponse{ body: b, status: status, header: make(http.Header), } }
I normally end up with a number of Response
implementations to handle different types of bodies, such as streaming from an io.Reader
, something that needs to be released after we've written it, or even something more tightly related to a database query which is able to return an array of results and paging information.
With this approach, an action's signature looks like:
func ListUsers(req *http.Request) Response { return NotFound }
We then create a wrapper function to glue the two worlds together:
// setup the route: http.Handle("/users", wrap(ListUsers)) func wrap(action func(req *http.Request) Response) func(http.ResponseWriter, *http.Request) { return func(out http.ResponseWriter, req *http.Request) { res := action(req) if res == nil { res = ServerError } res.WriteTo(out) } }
Admittedly, it's a small change. I do find that it improves readability and makes the flow much more natural though.
// let's pretend *http.Request has a Params map func ShowUser(out http.ResponseWriter, req *http.Request) { user, err := LoadUser(req.Params["id"]) if err != nil { serverError(out, "load user", err) return } if user == nil { notFound(out) return } // yes, could be turned into helper functions like the above // serverError and notFound out.Header().Set("Cache-Control", "public,max-age=60") out.Header().Set("Content-Type", "application/json") out.WriteHeader(200) //todo: serialize out.Write(body) } // VS func ShowUser(req *http.Request) Response { user, err := LoadUser(req.Params["id"]) if err != nil { return Error("load user", err) } if user == nil { return NotFound } return Json(200, user).Cache("60") }