Two steps forward, one step back; Web application frameworks
I’m trying to get back into writing (I get that once every few years), so here’s an experiment with just sharing some work experiences. In this series of posts, I’ll talk about a series of experiences where I switched technologies, instead of doubling down on them.
For context, I’m working on a relatively large project, a configuration management user interface for a specialized application in the mobile networking business. Oversimplified, it’s a CRUD application, a bunch of forms, a REST API, a database, and writers for XML and other configuration files. The complexity comes from the scale - dozens of models, hundreds of fields, each with specific validations, relationships, user experience challenges, and all this managing dozens of instances of our back-end application. The other part of the complexity comes from the fact that, at least for the moment, I’m the sole developer on the project.
I picked the Go programming language for the back-end and Typescript and React for the front-end. Early on I had to decide on what API technology to use between our front-end client and the back-end. Given that the API will eventually be consumed by our end-users, who aren’t as tech savvy as a dedicated engineering organization, I picked REST over modern alternatives like GraphQL. Another reason is that GraphQL is more aimed at use cases where you have multiple datasources and you can’t predict how these datacenters will be used, also known as a backend-for-frontend. That’s overly complicated for this use case, so we went for a REST API.
To serve a REST API, Go offers a HTTP server library in its standard library; for simple paths, this works fine, but it doesn’t come with route matching by default. Route matching means that the library will match incoming requests with a list of potential routes, parsing dynamic parts like IDs and passing them on to the route’s handlers automatically and securely. Go does not offer this functionality by default. In theory one can build this oneself in Go, but - especially at the time - I thought why reinvent the wheel? I’m not smart enough to do route matching, I’ll probably miss a lot of edge cases and security issues.
So as many developers would do, I went online to look for libraries. Of these, at the time, Gin was the most promising; I had used it before in various smaller ‘play’ projects over time, and it felt like a sane, well thought-out choice. So I installed it, set up my routes in Gin’s preferred style, and everything was fine.
There’s a “but” coming, but honestly, if you use something and it doesn’t hurt, don’t change it - a little pain is better than spending too much time changing things around. Think pragmatically.
After some time passed, things started to itch. Gin’s way of declaring routes and handlers does not match Go’s own http.Handler
or http.HandlerFunc
interfaces, which meant that any middleware or online code example I used had to be specifically adapted to work with Gin. That was mainly a minor annoyance though, I could live with it, it just required a bit more effort.
I decided to move away from it when I did some dependency analysis. By chance, I came across a tool called Goda, a Go dependency analysis toolkit. I ran it on my codebase at the time, and discovered that Gin added multiple megabytes to my generated binary, even though I only really used it for route matching.
Turns out Gin also added support (and therefore a dependency) to grpc, which in turn adds dependencies to https://developers.google.com/protocol-buffers libraries. This issue was reported here and here, the year before I actually used it, but it seems that issue was still present in the version I used.
But at the time, it was the proverbial straw. I switched to Gorilla, which is a competing web application framework that is fully modular, so it only pulls in the necessary functionality (it has no dependencies of its own). It’s also much closer to Go’s own HTTP handlers, using the same http.Handler
or http.HandlerFunc
interfaces so that there’s no friction between it, Go’s own code, online examples and tutorials, or external libraries - whereas for Gin, these resources would have to be specifically adjusted.
Ironically, I was already using Gorilla; Gin’s session management is a thin wrapper around Gorilla’s session management library.
The moral of the story: Bigger, more documentation and more stars doesn’t mean it’s better. This is a recurring theme when it comes to Go development and my personal experiences in Go, in that you don’t need these big, heavyweight frameworks. Every framework adds a layer of complexity, its own ‘language’, and especially in the case of Gin, hidden performance or binary size downsides due to hidden dependencies.
I mean granted, the particular issue with including unused libraries has been resolved since, but it still stung and while that issue was around, many people pulled and compiled in dependencies they didn’t need.
A frequently recurring advice on the various Go communities is “just use the standard library”; this is a reaction to people reaching for libraries and frameworks first, because that’s what they’re used to. Competing languages - Java, Javascript, PHP, Python, etc - are often not as feature-complete in their standard library, or their communities are not as opinionated on how to structure your application or their interfaces, so they quickly center in on various frameworks and the architectures they take with them - Spring for Java, Express for Javascript, Laravel for PHP, Django for Python, Ruby for Rails, you name it. These languages all have their go-to framework, and therefore their developers grow up thinking that you can’t use the language without the framework. From that experience, they roll into the Go ecosystem, and quickly try to find handholds in opinionated frameworks that tells them what to do, tells them not to worry about things like performance, security, enterprisey-ness.
But Go and its community go the other way. They favor pragmatism, they use adages like, “A little copying is better than a little dependency” or “clear is better than clever”, and a lot of less catchy idioms that basically boil down to YAGNI - You ain’t gonna need it. Web frameworks? But you’re only doing some route matching and JSON marshaling. Object-Relational mapping? But you’re only doing a SELECT * FROM table WHERE id = ?
. A code generator to generate interfaces with your legacy database or XML documents? But you’re only using a handful of fields. Those will actually be subjects of posts to come; watch this space.
Let me know what you think of this article on twitter @FrWielstra or leave a comment below!