Some Time to Iterate: Go Interfaces edition
One of the major benefits of taking time off work to play around with technology is the freedom to dig in deeply enough into something, making sure I really understand the principles, rather than just getting it working “well enough” and moving on. Being able to guiltlessly spend time experimenting and trying different approaches is refreshing — as nice as it might be to lean over and just ask someone for a sanity check, I’m really enjoying being able to leisurely return to the same solution, minutes or hours or days apart, to tweak and incrementally tighten up my code.
A related advantage of using side projects to learn is to gain the experience of putting something together from the ground up. While I worked with phenomenal teammates at Parse+Facebook, others often laid the groundwork — and working around a project with most of its bones in place is dramatically different than figuring out the skeleton (and the tradeoffs of early choices) on your own.
So! Onward, to a nicely simple illustration of the flexibility of Golang’s interfaces.
The Setup
We have a Go server that is capable of doing a bunch of specialized work, but should by default just serve an index.html
. We can assume that all requests that hit a particular subpath — /s/
, for example — should be handled by the more complicated logic, and everything else should fall through to being served statically. If a file doesn’t exist to be served statically, we should serve some visually appealing 404 instead (without changing the URL in one’s browser).
Fortunately for us, Go’s net/http
module has a nice FileServer
function that’ll serve static files from the filesystem for me. The current (naive) server flow looks something like:
This handles almost everything we want — requests to /s/
get handed off to our custom handler whereas everything else falls through to statically served files — except the default FileServer
404 behavior is to return a boring and aesthetically displeasing text “404 page not found” response.
The Solution
net/http
’s standard Handler
s are themselves an interface: meaning, any struct
that implements the required methods can be used wherever a Handler
is expected. In this case, the interface is defined as:
Great. Pretty easy to define our own StaticHandler
, and to define our own ServeHTTP
function. But we can’t stop there — we want to make sure that we actually identify a 404, intercept it, and return our custom error page instead. So in addition to implementing the http.Handler
interface, we’ll implement the http.ResponseWriter
interface, which is also fairly straightforward:
Here, though, we want our custom response wrapper to be able to capture the values the standard file server retrieves, then allow us to access or modify them as needed. So we do the bare minimum necessary to expose that data:
With this response
struct in my pocket, my StaticHandler
can now contain the sort of bait-and-switch-with-a-404 logic I’ve been hoping for:
Which gets me what I was hoping for: my custom 404 page rendered on Not Founds, without changing the path.
The Alternatives
A quick rundown of other ways this could’ve gone wrong we could’ve gotten this working:
- Use one of the many existing Go web frameworks. Not strictly necessary for my use case at the moment and would’ve obscured my goal of better understanding
net/http
. - Instead of wrapping an
http.ResponseWriter
, manually check whether a given filepath exists or not in the filesystem before asking the file server to serve the file. Ugly and potentially annoying to handle common situations like “/” redirecting to “/index.html” — just let the file server do its job and determine whether it’s a 404 or not. - Read the file from the filesystem on
StaticHandler
initialization and pass it as a[]byte
to the handler. Not bad but there’s no real reason to cache the page like that: serving an HTML page isn’t expensive enough to justify dirtying up the code. - Read the file from the filesystem and
.Write()
it yourself to thehttp.ResponseWriter
. Why would you, whenhttp.ServeFile()
handles that correctly for you? - Rewrite
r.URL.Path
to point at"static/404.html"
and callServeHTTP
again on the modifiedhttp.Request
. “Explicitly overwrite” should be sending up all sorts of red flags, and it feels dirty to have to callfileServer.ServeHTTP
twice. - Use a
http.Redirect
instead ofhttp.ServeFile
. Loses the URL fragment the user requested (and getting a 3xx status code doesn’t seem quite right from the client’s perspective either).
… though hopefully any of these approaches would’ve sent up some warning signs to any sort of code reviewer. In any case — play! Explore! See how many times you can revisit working code to make it feel as nice as possible.
Takeaway
There’s never one absolute way to do things, but trying a handful of different methods out lets us circle until we find the behavior that feels the best. (It’s also nice finding that this is the approach that bradfitz recommended, instead of yet another way to solve the problem — changing the behavior of http.FileServer
itself.)
Let me know what you think on Twitter.