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.
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.
Handlers 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:
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.
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
- 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
StaticHandlerinitialization and pass it as a
byteto 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 the
http.ResponseWriter. Why would you, when
http.ServeFile()handles that correctly for you?
r.URL.Pathto point at
ServeHTTPagain on the modified
http.Request. “Explicitly overwrite” should be sending up all sorts of red flags, and it feels dirty to have to call
- Use a
http.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.
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
Let me know what you think on Twitter.