3 gripes about Go modules
(Number 4 will drive you crazy)
Do you remember $GOPATH
? It’s from the era before Go modules came to be, where installing Go libraries used to be a manual, rather frustrating experience. (This also isn’t that long ago, actually.)
Say what you will about Go modules, but compared to the $GOPATH
times, they have been a literal godsend: installing, publishing, and finding packages has since become a standardised and convenient process.
So there really shouldn’t be anything to complain about. But you know how it is with everyday tools: as time goes by, we take them for granted, and at some point, the rough edges start to grate.
Here are 3 things that I wish Go modules would have done differently.
#1) The v2
debacle
Go modules are supposed to follow a semantic versioning scheme, where every breaking API change should increment the major version – e.g., from v3.4.1
to v4.0.0
.
However, even many seasoned Go modules with a long and undeniable history of backwards-incompatible API changes appear to be stuck forever at version v0.x.y
or v1.x.y
.
To understand why that is, you can conduct a simple self-experiment: publish your own Go module and naively try to bump its major version from v1.x.y
to v2.0.0
. Good luck!
The developer experience of this process is a major downer (pun intended). So a lot of package maintainers – presumably for peace-of-mind reasons – appear to stay away from v2
perpetually.
If either your frustration tolerance or curiosity is over-average, make yourself a cup of coffee and dive into the nooks and crannies of publishing a v2
module. I’m not saying there isn’t good reasoning behind this, and it also isn’t rocket science in the end. However, people obviously vote with their feet and opt out collectively, so there is that.
#2) Chaos in the project root
It might mostly be me here, but this one really drives me crazy.
In many code bases (independent of language), the directory structure of the project root looks something like this:
docs/
src/
.gitignore
.editorconfig
LICENSE.txt
Makefile
README.md
A typical project root is a wild hodgepodge consisting of various config files, documentation, build manifests, utility scripts, and what have you. The one reason that makes this madness bearable is the src/
folder: Heaven’s Door into the sane and sacred land of the actual source code.
Well, unless your project is a Go module. In this case, the base reference for all imports is the project root, so you are effectively encouraged to put your .go
files there as well.
That leaves you with three choices:
- Option 1: Yolo! Embrace the chaos and scatter your most essential source files all over the project root, right amidst random dot files, miscellaneous configuration, and whatever else happens to float around there.
- Option 2: Create a
src/
subfolder anyways. In this case, however, all client code will have to alias each and everyimport
statement, otherwise the imported code symbols carry a rather genericsrc
prefix (as in,src.Connect()
instead ofdatabase.Connect()
). - Option 3 (this is what I do): work around.1 Create a subfolder for all the code, and name it like the module itself. That may look slightly unconventional at first, but it lets you stash away your code tidily, while retaining the “correct” default prefix for imported symbols.
I would have found it nicer if the go.mod
file supported a dedicated directive to specify the base reference for resolving imports. That would have been sufficiently clear, but also given maintainers more flexibility to organise their project.
#3) github.com
all over the place
Doing a fulltext search for github.com
in a moderately large Go project will yield a lot of matches all across the project. The reason is threefold:
- When importing a Go module in a
.go
source file, theimport
statement references the module’s full path. - The module’s path contains the origin – a.k.a., the hosting location.
- Many modules are hosted on
github.com
.
package main
import "github.com/hello/world"
func main() {
world.SayHello()
}
While it’s nice that the Go package manager encourages decentralized hosting, it’s both distracting and redundant to see the full module URLs everywhere in the code base.
Furthermore, if you were to move your module from one hosting provider to the other (e.g., from github.com
to gitlab.com
), then everyone would have to change all corresponding source files that reference this module.2
In my mind, the hosting location is an implementation detail, so to me it would have been more sensible to decouple and hide that information altogether. E.g., the modules’ download URLs could have been specified just once per module in the go.mod
file. That way, I think the import
statements would have been more concise and meaningful.
-
See this project for an example. The
import
statements are still somewhat redundant, but to me it’s the best tradeoff overall. ↩︎ -
You could try to work around this via the
replace
directive, but that only solves the problem temporarily. You could also provide your own proxy, but who does that? ↩︎