SamWhited|blog

Photograph that needs minor corrections such as leveling.

Remaining Problems With Go Modules

WARNING — this post is mostly me blowing off steam about problems that likely won't be fixed given that the damage is already done, or even just harping on community problems that have already been fixed. This may be tiresome for some; so feel free to avoid it if you're enjoying using modules.


Though I was skeptical of Go Modules when I first heard Russ float some of his ideas about versioning at the first Go Contributors Summit a little over 2 years ago, I was proven very wrong when the actual vgo prototype was released. Since switching my projects over to modules, things I do on a day-to-day basis like maintaining dependencies, and building packages for various OS package management systems quicker and easier.

However, I feel that modules have been rushed into production and shouldn't have made it into a Go release yet. Since modules are becoming official in Go 1.12, I don't think the problems with it will hamper adoption much (as they might if this were a third party tool), but I do wish the Go team would reconsider forcing modules through in two versions without making a few big changes.

The file format

TL;DR — Just use TOML or YAML or just about anything else…

I love the go.mod file format. I hate the go.mod files. Inventing yet another file format is an absolute non-starter for me when there are plenty of technologies that could easily represent the data in go.mod files and are also perfectly human readable. Shortly after Modules were released I decided to write some tooling to analyze the dependencies of my projects. If the module format had been written in TOML, or XML, or even YAML (which I deeply despise more than just about every other serialization format) I would have been able to import a widely supported library and not have to write my own parser and hope that my assumptions about this undocumented and unstandardized format held. What's more, I could have written my tooling in any language I wanted; I could have thrown together a quick script in Python, or even BASH, I could have built a robust and safe tool in Rust, or I could have made it work on the web or off with Clojure and ClojureScript. With modules choosing to make up its own format, none of this was possible and the world has to suffer through yet another custom serialization format. Personally, I really like the format, but that's no excuse for indulging Google's characteristic not-invented-here syndrome.

Major version in import paths

TL;DR — Adding the major version prefix to the import path is confusing and difficult.

This to me is the biggest mistake modules make. So far I have seen at least three major projects fail to put major versions in their import paths, meaning that they end up importing older versions of themselves or, depending on where they left it out, others who import their package get a pseudo-version (v0.0.0-timestamp-commithash) even though they should be importing a proper version. The upgrade path is terrible too, for example when I attempted to upgrade the Stripe SDK, a rather large project, I ended up changing 150 files, and ~340 import lines. I know that there will likely be tooling to do this, but it still doesn't change the fact that there are now merge conflicts due to import statements all over the codebase, and we will end up having them again every time there is a major version bump. I also think the tooling needs to exist before Modules become the official solution (and probably be built into go mod init to make the transition easier).

The argument I've heard made for why this doesn't matter is that you shouldn't be making major version bumps in the first place, and that if a project does bump the version constantly no one will use it anyways. Unfortunately, we use Stripe so I'm rather stuck using the Stripe SDK which is now on major version 55, and will probably be on 63 by next week sometime. As much as I wish they'd stop changing their API and bumping the version, that doesn't seem likely to happen in a post module world.

There's also the problem of how we add major versions to the import path. This is arguably more important since we likely have to Because it looks like part of the file path (eg. package/v2), I've seen several people say “you have to copy/paste your code into a new tree and can't use branches anymore”. I corrected this notion, but you can see how they'd arrive at it. This is needlessly confusing, especially when the module files themselves use an entirely different format in the import path (eg. package v2). My guess at why this wasn't chosen is because there could in theory be multiple modules in an import path (package/v2/subpackage/v3), but this seems like such a complicated and bad idea that forbidding nested modules seems fine.

It may also be be that the path-like nature of the major version was deliberate so that both the copy/paste into a new tree method and the branch or tag method was possible, but since you still have to make major version tags anyways, and there's no guidance on what's best or how to use what version, this feels very un-Go-like. Having multiple ways to manage multiple supported major versions just seems like unnecessary complexity that will lead to confusion about how to contribute to projects and find what code you're actually using.

# No syntax highlighting or automatic indentation here because of
# not-invented-here syndrome.
# See the previous section for more complaining on this topic.

# What's wrong with this?
module mellium.im/cli v2

# In a go file it could be:
# import (
# 	"mellium.im/cli v2"
# )
#
# Which matches this:
require golang.org/x/text v0.3.0

# And this:
replace golang.org/x/text v0.3.0 => golang.org/x/text v0.0.0-20181030141323-6f44c5a2ea40

Conclusion

Since modules were widely criticized for sidestepping the community entirely and being designed and implemented behind closed doors, the Go team has tried much harder to involve the community earlier in the development process for new features (eg. see the various draft designs). However, this didn't stop them from continuing to push modules through with little to no input from the community. Overall, Go Modules are a joy to use, but I suspect as they gain widespread adoption we will find more problems, more serious and uncorrectable design flaws, and more places where community involvement could have made Modules a better product. Then again, who knows, maybe I'm the only person who has concerns about these issues and the community will embrace modules with open arms and the speed at which they were rammed through the process will be justified by the time savings the community gets from finally having reasonable dependency management. I'd rather not find out that I'm right, so let's hope that it's the latter.

All that being said: please adopt modules in your projects. Let's finally get the community standardized on one tool.