Imagine you’re a beginner writing a Web Service or web application in Go. Everything is going fairly smoothly, right up
until you realize your HTTP handlers are going to need to access a database. The http.HandlerFunc
interface is set by
the Go standard library:
type HandlerFunc func(ResponseWriter, *Request)
There’s no obvious way to add more arguments, so what can we do?
I’m going to run through three different ways to solve the problem: the object-oriented approach, a more functional approach, and a third variant using an interface and an adapter function.
First, though, let’s run through a simple example. Check out global.go, the code for a simple POST/GET/PUT Web Service for short messages.
Note that since this is an example, it’s written to be as short and straightforward as possible, rather than robust or elegant. The error handling leaves a lot to be desired, there’s no MIME type checking, and in a real web service you’d probably abstract out things like decoding the request.
The main()
function is easy enough:
func main() {
var err error
db, err = sql.Open("pgx",
"postgres://localhost:5432/example?sslmode=disable")
if err != nil {
panic(err)
}
router := makeRouter()
err = http.ListenAndServe(":80", router)
log.Fatal(err)
}
In this example I’m using the excellent Chi router to make decoding easier, and PostgreSQL as the database, but the same principles apply for any router or database. I’m also using the standard library’s SQL API to avoid any confusing Postgres-specific features.
The makeRouter
function is also easy enough:
func makeRouter() http.Handler {
r := chi.NewRouter()
r.Get("/msg/{id}", getMessage)
r.Put("/msg/{id}", putMessage)
r.Post("/msg", postMessage)
return r
}
The individual handler functions then all follow the same pattern:
func getMessage(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
// Do stuff with database and return HTTP response
// ...
}
There’s just one problem with this code, and it’s right at the top of the file:
var db *sql.DB
It’s a global variable! Eww!
So, how do we get rid of the global variable, but still keep the code working?
The object-oriented approach
I’m going to start off with the object-oriented answer to the problem, because I think it’s the simplest to understand.
First of all, we create an object (struct) type which we will use to hold our application-wide state:
type Application struct {
db *sql.DB
}
We then change our handlers to be methods on the Application
object:
func (app *Application) getMessage(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
// ...
row := app.db.QueryRowContext(r.Context(), ... )
// ...
}
Because the handler is a method, it can now access the sql.DB
as app.db
.
The final changes are to remove the global, make main
pass an Application
to makeRouter
, and have makeRouter
call the methods on the Application
it is passed:
func main() {
db, err := sql.Open("pgx", "postgres://localhost:5432/meta?sslmode=disable")
if err != nil {
panic(err)
}
app := &Application{
db: db,
}
router := makeRouter(app)
err = http.ListenAndServe(":80", router)
log.Fatal(err)
}
func makeRouter(app *Application) http.Handler {
r := chi.NewRouter()
r.Get("/msg/{id}", app.getMessage)
r.Put("/msg/{id}", app.putMessage)
r.Post("/msg", app.postMessage)
return r
}
You can see the full code in object.go.
The advantage of this approach is that it makes for clean and uncomplicated code. Your application’s handler functions might need a database pool, a logger, some templates, configuration information read from a file, and all kinds of other runtime state, but the router code can remain simple as it’s all encapsulated in the method receiver.
The downside, of course, is that the Application
object can become a dumping ground, and there’s no way to
tell from the method signature which fields of the object any given handler might need. That, in turn, can make
it more problematic to write unit tests, and couple them to the implementation — do you set up a complete Application
with all its fields for every test, or just set up the fields you’ve worked out that the handler needs from reading
through all of its code?
Another pitfall is that you might also be tempted to code the database-opening logic into the object, perhaps via a
NewApplication
constructor. I recommend not doing that – it’s better to do dependency injection, so the Application
doesn’t have to know or care what kind of database it’s passing around; a principle known as separation of
concerns. That way you can easily pass in a mock sql.DB
for your unit tests, or have the application support
multiple types of database chosen at run time.
I also suggest that you don’t try to make the object a singleton. There’s a good chance you’ll want to be able to construct multiple instances of it when writing unit tests; maybe not for database access, but for other pieces of application-level state.
The functional approach
Now let’s look at a completely different way to tackle the problem.
This time we’re going to use partial application and a closure to store the application-level state. Or in less fancy terms, we’re going to write our handlers by writing functions which take some state and return handlers which operate on that state.
So for each handler, we’ll transform from something like this:
func getMessage(w http.ResponseWriter, r *http.Request) {
// Have to use a global if you want db access here
}
to something like this:
func getMessage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Inside here we can still access the variable db from the outer function!
}
}
What we’re doing above is taking a function that really needs three arguments, and manually turning it into a function which applies the first argument and returns a function which will apply the remaining arguments.
Once we’ve done that, in the makeRouter
function, we’ll swap this:
r.Get("/msg/{id}", getMessage)
for this:
r.Get("/msg/{id}", getMessage(db))
The db variable can just be passed in from main
as a regular argument to makeRouter
.
The end result of the refactoring is in function.go.
The advantage of this approach is that everything is explicit. If your wrapped handler only needs a few pieces of application state, the wrapper function can skip the rest. This is good for unit testing, and also for refactoring — if I refactor a wrapped handler and it no longer use one of the state variables, my IDE will tell me I have an unused function argument in the outer function and I can remove it.
The downside of this approach is the same: that everything is explicit. If you have eight pieces of application-level runtime state and your handler needs all of them, you’ll have to pass them all as arguments.
You might also dislike the fact that every handler is now wrapped in an extra function, and be wondering if there isn’t a way to avoid that extra code. Which brings me to the third approach..
The interface extension approach
Imagine for a moment how easy this would all be if the http.HandlerFunc
type included a sql.DB
parameter. Well, we
can make that happen, or at least something like it. We start by defining our own extended version of the HandlerFunc
type, and giving it any extra arguments we want, in this case a sql.DB
:
type ExHandlerFunc func(db *sql.DB, w http.ResponseWriter, r *http.Request)
Now we can write each handler as an ExHandlerFunc
instead of a HandlerFunc
:
func getMessage(db *sql.DB, w http.ResponseWriter, r *http.Request) {
// Code as before
}
The only problem is that our router wants a HandlerFunc
for each route. We solve that problem by writing an adapter:
func withDB(db *sql.DB, xhandler ExHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
xhandler(db, w, r)
}
}
The adapter takes a database pool and an extended handler, and returns a regular handler which calls the extended handler
with the database pool. The reason for the name is more obvious when we update the makeRouter
code:
func makeRouter(db *sql.DB) http.Handler {
r := chi.NewRouter()
r.Get("/msg/{id}", withDB(db, getMessage))
r.Put("/msg/{id}", withDB(db, putMessage))
r.Post("/msg", withDB(db, postMessage))
return r
}
The finished code is in interface.go.
This is really a variation on the function wrapper technique, we’re just generalizing the wrapper so that the handler implementation doesn’t have to have it visible.
The advantage of this technique, as with the previous one, is that in the (extended) handler code, all the state is
explicitly supplied as arguments. When it comes to unit testing, there’s no Application
object to create. There’s a
little more complexity in the router building code, but not much.
The downside is that this technique won’t work so well if your handlers need widely varying selections of application state. You’ll either end up with lots of arguments you don’t always use, or you’ll have to define multiple extended handlerfunc interfaces with different combinations of state variables as argument. Still, it’s a useful technique to be aware of for some situations.
Conclusions
So, three different ways to solve the same problem. Which is best? As is usual in software engineering, it depends — there are advantages and disadvantages to each approach, and you’ll have to consider the tradeoffs and decide which is best for your specific application.