RESTful routing in Go
May 05, 2015
A few days ago, I wrote about wrapping Go HTTP handlers so that they can return an abstract Response object rather than dealing with http.ResponseWriter
. Today I want to focus on the other end of the request: routing.
What I'm doing here should work with any of the many third party HTTP routers (aka multiplexers) that support URL parameters. I'll be using my own, but adapting it to something else should only require a few small changes.
An anti-pattern that I've seen from the Go community is defining specific routes as opposed to using general patterns. I'm by no means dogmatic when it comes to URL patterns, but I do think having consistent and human-readable URLs is beneficial.
# bad
GET /users
GET /users/:id
# good
GET /:resource
GET /:resource/:id
Ultimately, goal is to end up with a map of resources + action names (users show, users favorite list, sessions delete) to handler functions. As a first step, we can manually create this mapping:
type Action func(out http.ResponseWriter, req *router.Request)
type Resource map[string]Action
var resources = map[string]Resource{
"users": map[string]http.Handler {
"show": users.Show,
"list": users.List,
"listfavorites": users.ListFavorites,
}
"sessions": map[string]http.Handler {
"create": sessions.Create,
"delete": sessions.Delete,
}
}
Next, we define generic routes:
r := router.New(router.Configure())
r.Get("/:resource", ListAction)
r.Get("/:resource/:id", ShowAction)
r.Get("/:resource/:id/:child", ListChildAction)
r.Delete("/:resource/:id", DeleteAction)
With these two pieces, we can invoke the proper handler:
func ListAction(out http.ResponseWriter, req *router.Request) {
resource, exists := resources[req.Param("resource")]
if exists == false {
return
}
action, exists := resource["list"]
if exists == false {
return
}
action(out, req)
}
We can refactor the XYZAction
functions and have a single RestDispatch
handler:
func ListChildAction(out http.ResponseWriter, req *router.Request) {
RestDispatch(out, req, "list"+req.Param("child"))
}
func RestDispatch(out http.ResponseWriter, req *router.Request, actionName string) {
resource, exists := resources[req.Param("resource")]
if exists == false {
return
}
action, exists := resource[actionName]
if exists == false {
return
}
action(out, req)
}
This is probably good enough. However, we can use reflection to improve the manual mapping:
type Action func(out http.ResponseWriter, req *router.Request)
type Resource map[string]Action
var resources = make(map[string]Resource)
func Start() {
r := router.New(router.Configure())
r.Get("/:resource", ListAction)
r.Get("/:resource/:id", ShowAction)
r.Get("/:resource/:id/:child", ListChildAction)
r.Delete("/:resource/:id", DeleteAction)
registerActions(users.List, users.Show, users.ListFavorites)
registerActions(sessions.Create, sessions.Delete)
}
func RegisterAction(actions ...Action) {
for _, action := range actions {
fullName := runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()
relevant := strings.ToLower(fullName[strings.LastIndex(fullName, "/")+1:])
parts := strings.Split(relevant, ".")
if len(parts) != 2 {
panic("action " + fullName + " should be in the form of package.name")
}
resourceName, actionName := parts[0], parts[1]
resource, exists := Resources[resourceName]
if exists == false {
resource = make(Resource)
Resources[resourceName] = resource
}
resource[actionName] = action
}
}
It's small improvement that further enforces consistent naming and reduces some of the magic strings. Unfortunately, in Go, it isn't possible to scan a package for functions of a given signature. If it was, we'd be able to automatically detect and register actions.
Finally, this approach doesn't exclude the possibility of having one-off routes for cases that don't fit the general patterns.