Go Adapter Pattern Part 3: Service Handlers
This is the third in my series of posts showcasing how to use the “adapter pattern” in Go. If you missed the earlier entries in this series, you should take a look at Part 1 and Part 2. This is the third and likely final component of the series; I’m going to show how you can structure handlers on your HTTP server in a way that plays nice with the patterns described in the previous posts. The inspiration for this post comes from Mat Ryer’s talk here.
To start, remember that http.HandlerFunc
implements the http.Handler
interface, which means we can use regular old functions (and methods!) as HTTP handlers. It will become clear why this is so convenient in a bit. I’m going to reiterate a lot of what Mat outlines in his blog post because I like to structure my services in the same way. In short, I have a server
type, and all the dependencies for my service hang off of that:
type server struct {
repository *someDatabase
router *someRouter
notifier Notifier
}
I also have a routes.go
that contains all my routing code. The following is a little simplistic for example’s sake; Part 2 for how to structure this in a slightly nicer way:
func (s *server) routes() {
s.router.HandleFunc("/", s.handleIndex())
s.router.HandleFunc("/users", s.handleUserIndex())
s.router.HandleFunc("/users/{username}", s.handleUserGet())
}
You may notice that the second argument to HandleFunc
is actually the return value of the service method; the methods actually return the handler. This is nice because it provides a closure environment; you can set stuff up in the closure environment, define local types, and also pass dependencies into the service method and reference them in your handler function. Here’s an example of what that might look like:
func (s *service) handleUserGet() http.HandlerFunc {
type sanitizedUser struct {
Username string `json:"username"`
}
type response struct {
User *sanitizedUser `json:"user,omitempty"`
Error string `json:"error,omitempty"`
}
return func(w http.ResponseWriter, r *http.Request) {
var resp response
routeVars := mux.Vars(r)
if _, ok := routeVars["username"]; !ok {
resp = response{Error: "No username!"}
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(resp)
return
}
u, err := s.repository.GetUser(r.Context(), routeVars["username"])
if err != nil {
resp = response{Error: err.Error()}
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(resp)
return
}
user := &sanitizedUser{
Username: u.Username,
}
resp = response{User: user}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
return
}
}
In this example the response types are defined in the closure; this is nice because it avoids cluttering up your package namespace and keeps everything close to where it’s needed. Mat outlines a couple other opportunities to leverage here like doing expensive setup operations with sync.Once
, and passing dependencies into the service method and using them in the handler; I won’t go into as much detail here, but take a look at his examples if you’re interested in this sort of thing.
Finally, the resulting server is very easily testable (using matryer/is
for lightweight testing):
func TestHandleUserGet(t *testing.T) {
is := is.New(t)
srv := server{
repository: mockDatabase,
}
srv.routes()
req := httptest.NewRequest("GET", "/users", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
is.Equal(w.StatusCode, http.StatusOK)
}
That just about covers everything I wanted to cover here. I’ll probably take a break from Go related posts for a bit. I’m planning a few posts about Go’s concurrency primitives, synchronization patters, and I’d like to do a series on the temporal
workflow engine basics, but before I delve into that world I’d like to do a couple posts on some machine learning basics, so keep an eye out (or let me know) if that sounds interesting!