I’ve been programing for over 20 years in varying professional roles at this point, and there are a few things I’ve learned along the way that helps me keep things straight, especially when working with other developers, or in code bases where I tend to come back after a while away to pick it back up again.
I’m a firm believer that when you write code, you should make everything as obvious as possible. This means no magic global variables, clever tricks, or hidden dependencies. I also think that you should rely on the compiler rather than tests to verify as much as possible, so in a strictly typed language like GoLang you would not rely on passing dependencies through as non-typed to be cast to the correct type further down the callstack. It often leads to runtime errors, and can make testing a lot more difficult than you would like as you need to know all the hidden dependencies.
Mat Ryer wrote an excellent blog posts called
How I write HTTP services in Go after 13 years
covering a whole list of things he does and has learned through his career. If you haven’t read it
yet, it’s worth a look. One of the things he listed was making his handler functions return a
http.HandlerFunc
rather than implementing it directly. This is also an excellent way to pass
global dependencies like a database connection to the handler.
|
|
You can also do this using a struct that takes the dependencies, or even a struct that implements the Handler interface:
|
|
I tend to go with the first option, as I like to keep internal dependencies internal to the struct
as much as possible and use a New<Struct>
method to set up the struct. This means they can’t be
access outside the struct and changed, making them immutable. e.g. I can’t change the dbConn
reference later without creating a new struct. And since I would be calling a function to set up the
handler, I might as well just call a function directly. This also makes it much easier to navigate
directly to code being executed in my IDE, rather than having to know that mux.Handle()
calls
ServeHttp()
on the struct and then find that implementation.
Request Scoped Dependencies
The approach above works well for global dependencies, but what about dependencies that are scoped
to a request? I often try to add a request id to log entries for a request, so I can easily group
them together. I use the With()
method on slog.Logger
to add a parameter to all subsequent log
write calls. It creates a new instance of the logger that can be passed through to any further
methods that needs to write log entries.
I use the following method to set up a new logger with a parameter
|
|
This can be expanded to check a header to pull a request ID from an API Manager, a load balancer, or an upstream system that you would like to be able to trace logs across.
I could call this method on the beginning of every http.HandlerFunc
method, but I know I will
forget and there is also no simple way to test this. So how can we pass this request scoped
dependency through to the handler logic?
A common approach is adding middleware that injects this into the request context with the
WithValue()
method and makes it the new context for the request. Then in the handler method, we
can grab it, cast it to the correct value, and use it as expected. This is both quick and simple,
but has a few obvious drawbacks. Main one in my opinion is that the validation of the type happens
at runtime rather than during compile time.
I prefer an alternative approach that overrides the function signature for http.HandlerFunc
by
creating a new function type with *slog.Logger
as a parameter. And then wrap all the handlers in
a function that sets up and injects the request scoped dependencies
|
|
This gives us a clean implementation that can be verified by the compiler, and it can easily be
expanded by modifying the signature of HandlerWithRSD
with something like a session object, and
then updating theRequestScopedDepdendencyInjector
struct and methods to account for the new
features.
My favourite part of this approach is that the compiler will tell you if you missed any handler methods that needs to be updated with the new parameter.
To use this with your routes, you simply have to wrap your routes with the WithRSD()
method like
below:
|
|
With a bit more setup with log output format and a database connection, it will print the following
to stdout. You can easily see which log entries belong together based on the request_id
value.
If you use a log aggregation system, you can group log entries based on this across multiple
services by passing the request id to downstream systems.
|
|
I’ve created a full working example implementation of this on my GitHub. Any suggestions and improvements are more than welcome.