I’ve been part of a conversation revolving around how modules have been integrated with the https://github.com/gofrs/uuid repository. We had an issue filed stating that there was not one standard import path that worked for both a non-module and module user-base if you were a library author who was using the package.
The issue made a few interesting points that I wanted to investigate.
The troubling points to me for the module resolution were:
- A user who was running without modules and required the path
- A user who is running with
GO111MODULE=onand requires the path
- A user who is running with
GO111MODULE=onand requiring the path
NOTE: There is another user group not covered by the above cases. A user who is running a version of Go without “minimal module awareness”. This was added in Go 1.9.7+, 1.10.1+, 1.11+. For this user, the
github.com/gofrs/uuid/v3 import simply doesn’t work.
Conclusion: There wasn’t a single import path that gave the same experience to both groups of users.
Well as newly initiated to modules, this didn’t make a whole lot of sense. Our root had adopted modules at version v3.1.1, so why was the root package import returning an older version?
The clear defining point to me was to stop thinking about what the tags were on the repository, and instead, think about it from the concept of Semantic Import Versioning which is defined here.
gofrs/uuid added a
go.mod file in the root of its file tree, we were running in an incompatible mode as far as modules was concerned. Everything was blanket considered “+incompatible”. It is unclear exactly what this means, but it was casually said that it wasn’t a big deal to be considered incompatible for users to use your package in a module-based world.
go.mod defining that we start our definition at
v3 as we had been a good citizen and were already using Semantic Versioning, so we matched our module definition appropriately. This is where things get harder to follow and pull apart.
After adding the
go.mod file (because we defined it as v2+), everything previous to the addition of the
go.mod was considered the v0/1 range.
Untested, but my understanding is that since there is no commit with a v2 definition it is undefined. Therefore my assumption would be that it would also resolve to the same point, the last place before v3 was defined. Which would functionally make the
github.com/gofrs/uuid/v2 import equivalent. Once go.mod was added, we entered the
vDefined world of modules.
From this standpoint, the behaviour of modules in the context of #61 makes sense. The v0/v1 versions resolves to the last release before the addition of
What to consider
go.mod file was added without understanding the repercussions to a secondary tiers of importers. A single module user can import at either path and since they know whether they are using modules, they can use the correct path. This is not true of the secondary importer.
We can fix this by making the new Semantic Import path non-virtual but that is a varying amount of effort based on the package and will become unnecessary as the minimal module awareness versions of Go become the low-end of support.
It was recommended that the act of adding modules support is easiest if you mark it as a breaking change and accompany the addition of the file with a major version bump. This feels wrong.
I believe the above advice should be clarified whether this is a “current best-practice” that becomes out of date advice once only module aware versions of Go are in the support path.
My thoughts on this topic are all based from the viewpoint of supporting older versions of Go. Many of the worts we ran into would not be an issue if we chose to cut support to only users who had a minimal module supported version.
Here are the list of things you are opting into as a library dev when you add a
go.mod file for old versions of Go (assuming you don’t want to break an importer):
- You opt-in to supporting module paths even if you decide to later remove module support.
- You also are opting into making sub-packages that represent the state of your entire library API at all major versions as real folders (v2+).
- You can only alias (Go 1.9+) the most recent major version easily. Additional major versions require duplicate code, and back-porting security fixes to be a nice OSS citizen.
If you are considering opting into the module experiment and haven’t considered the above I highly recommend you do.
The current solution space makes use of type aliases to reduce the amount of duplicate code, but this is a Go 1.9 feature. By simply waiting for older versions of Go to age-out of usage / support, your first-time experience with modules will be much better. The need to create real versioned sub-package paths will diminish. If however, you already do all of the above in an existing semantic nature (back-porting fixes to branches is the big one.), adding the additional folder wouldn’t be much of a lift.
If as a library author you aren’t doing semantic versioning, I would start doing that and wait for Modules to settle and the tooling to evolve, and would recommend opting to NOT participate in the modules experiment.
If you already are doing semantic versioning, don’t depend on external dependants, and choose to support older Go versions as a library maintainer I would recommend opting to NOT participate as you are creating work for yourself for little to no benefit for your users.
If you are already doing semantic versioning, and you have a lot of external dependencies you don’t manage, I could see an argument where opting TO participate in the module experiment leads to a large benefit with regards to shared builds that follow the MVS resolution algorithm for “high-fidelity builds” that outweighs the current maintenance cost you as a library developer take on for consumers. But see the above caveats about what you are opting into supporting if you support older Go versions.