Go actions responses
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")
}