March 26, 2018·6 min read·engineeringgomodulesdependencies

Go package versioning starts with import compatibility

The package versioning proposal uses distinct import paths for incompatible APIs and minimum dependency requirements to make builds predictable.

An incompatible dependency update can stop a program from building even when its own source has not changed. The difficulty becomes sharper when two of its dependencies need different generations of the same library. A version number records the disagreement, but the package manager still needs a rule for constructing the program. Go's package versioning proposal puts that rule in the identity of the imported package.

Russ Cox's March 26 explanation describes an official proposal and the experimental vgo command behind it. The proposal itself was filed the previous week, following public discussion of the prototype in February. This remains a proposal. Its significance is the argument that compatibility should constrain the dependency model before the tool tries to choose versions. The prototype makes that argument concrete enough to evaluate against real package trees.

I like this direction because an import should tell a reader which API the source expects. Go already asks programmers to make many dependencies explicit in ordinary code. Making an incompatible API change visible in the import path extends that habit to dependency upgrades.

One import path carries a compatibility promise

The semantic import versioning explanation, published in February, gives the proposal its central rule. A newer package using an existing import path must remain compatible with the older package at that path. A breaking API needs a different path. Semantic versioning provides a systematic way to choose that new name.

For major version two and above, the path includes the major version. The convention permits a package under example.com/library and another under example.com/library/v2 to appear in the same program. They are different packages with distinguishable names. A tool does not need to infer which incompatible meaning a particular import intended from hidden resolution context.

This changes what a library author must do when publishing a breaking change. Incrementing a version tag does not complete the job. The source must identify the new module and use the corresponding paths for imports inside it. That is additional work for the author, but it gives downstream code an explicit choice about when to adopt the incompatible API.

Suppose an application depends on a reporting library and a storage library. Both use a serialization package. If the reporting library adopts an incompatible serialization API first, it should not force the storage library to change immediately. Distinct import paths allow the application to include both generations while each library moves on its own schedule.

The example also explains why compatibility cannot be reduced to whether two versions happen to compile separately. The application has to combine them. If a wrapper exposes another package's types, that relationship becomes part of the wrapper's public contract. Changing the wrapped package's identity may therefore require a corresponding change in the wrapper. Naming the distinction makes the dependency visible to both the compiler and the person maintaining it.

A module is the versioned unit

The proposal's module design groups packages into a unit that receives a version. A go.mod file identifies that unit and records requirements on other modules. A package remains the thing imported by a Go source file, while a module is the thing the version-selection machinery chooses.

That distinction avoids treating every package directory as an independently released product. Several packages can evolve together under one module version. It also avoids equating a repository with exactly one module. The design explicitly considers repositories containing independently versioned modules, including subdirectories with their own go.mod files.

An illustrative declaration in the prototype's format is small.

module example.com/report

require rsc.io/quote v1.5.2

The declaration names a requirement. It does not mean that every larger build containing this module must select precisely that version of the dependency. Another module can require a newer compatible version. Understanding that point is necessary before reading go.mod as a conventional list of exact pins.

The proposal also separates release packaging from source-control layout. Its module design allows a major version to live on a separate branch or in a major-version subdirectory. The module declaration identifies the intended path. Those options let projects retain different repository practices without changing what an import means to a consumer.

Minimum requirements produce a determinate build list

The minimal version selection design specifies how the requirements combine. Each dependency requirement names a minimum version. The tool traverses the requirement graph and retains the newest required version for each module identity. It need not choose the newest version published anywhere.

Consider a deliberately small example. The application requires library B at version 1.2, and B requires library D at version 1.3. Another dependency C requires D at version 1.4. The combined build must satisfy the stronger requirement, so it selects D 1.4. Publishing D 1.5 alone does not change that result.

This explains the word minimum. It is the lowest selection that satisfies the requirements accumulated across the graph, not the oldest release of each library and not necessarily the version first written by the application author. A requirement introduced through another dependency can raise the result. That makes transitive requirements part of the explanation for a selected version.

The compatibility rule is what permits this operation to stay simple. A later version under the same identity is expected to satisfy earlier users. If dependency metadata routinely excluded arbitrary later versions, choosing a build would require a different constraint problem. The proposal deliberately adopts a stronger compatibility expectation to avoid that model.

This expectation cannot prove that a release is free of regressions. A version-selection rule decides which code to build, while tests and review establish confidence in that code's behavior. A maintainer can still make a compatible-looking release with a bug. The design's contribution is a predictable selection rule and a visible way to name intended incompatibility.

Repeating a build differs from upgrading it

The prototype's tour shows dependency discovery and upgrades as different activities. A build can discover a missing import and add a module requirement. Once the requirements exist, the mere appearance of another upstream release does not force an unrelated rebuild onto it.

An explicit upgrade changes the requirements that future builds use. The tour also demonstrates downgrading a dependency, which can require downgrading another module that needs its newer API. That consequence follows from the requirement graph. A request for an older dependency cannot leave a higher minimum requirement silently unsatisfied.

The distinction helps explain why a small requirement file can reproduce version selection without a separate lock file in this design. The selected versions follow from the requirements and their dependency graph. New releases outside that graph do not become preferred merely because they are available. This is a property of the selection algorithm, not an assertion that availability or source integrity has ceased to matter.

I find that separation useful because an upgrade deserves a deliberate change to review. A command that rebuilds unchanged source should not also make an unrequested choice about newly published library code. The prototype gives those two operations different effects on the recorded requirements.

Source integrity is another part of repeatability

The February discussion of reproducible and verified builds distinguishes choosing a version from checking its contents. A version label alone cannot detect a tag that now points at different source. The prototype can record hashes of downloaded module trees and compare them against expected hashes.

Its demonstrated verification file is named go.modverify. That is a prototype detail, and the article explicitly allows the final form to change. Treating its spelling as a settled long-term interface would go beyond what the proposal promises. The mechanism matters because it checks a different property from selecting the same version number.

The same article warns that hashes reported inside a binary are self-reported build information. They help identify the intended inputs but do not independently authenticate a binary supplied by someone else. These are separate claims, and combining them would overstate the protection available from the prototype.

The proposal asks the ecosystem to adopt a compatibility convention as well as a tool. Projects considering it need to examine their public package identities and how incompatible APIs reach their consumers. The available vgo implementation gives them a way to exercise those decisions now. Feedback about cases where the naming and requirement rules fail can still affect the design before integration into the standard go command.