skip to content

How do you publish version v1.4.0 of a Go library so other teams can go get it?

level: juniorimportance: should knowfreq 50%

answer

  1. no registry, no upload
  2. git is the release mechanism
  3. the tag name is the version
  4. leading v, then semver
  5. verify on an empty module cache

basics

~20 s

Publishing a Go module means pushing a Git tag, not uploading an artifact. Commit a go.mod whose module path matches the repository URL, then push a tag named exactly v1.4.0. Consumers then request that module path at that version.

solid answer

~50 s

There is no publish command and no account to register. A Go module version *is* a tag in the source repository: commit `go.mod` with a `module` line equal to the path people will import (`github.com/acme/logkit`), then `git tag v1.4.0` and `git push origin v1.4.0`. The leading `v` is required and the rest must be valid semantic versioning; `1.4.0` or `release-1.4` are simply not versions to the go command. Consumers run `go get github.com/acme/[email protected]`, which resolves the path to the repository, reads the tagged commit's `go.mod`, and records the version and a content hash in their `go.mod` and `go.sum`. The version is whatever the tagged commit contains, so tag only after the `go.mod` change is committed — and verify on a machine with an empty module cache, not on the laptop that already has the code.

code

mod · 1 line
mod
module github.com/acme/logkit

go deeper

for a junior

Be ready to say plainly that a module version is a pushed Git tag named vX.Y.Z, and that the go.mod module line must be the path people import. Knowing there is no upload step is most of the answer.

for a middle

Explain the ordering that makes it safe: commit, tidy, then tag; and explain what the consumer's go.mod and go.sum end up holding after the fetch. Mention that lightweight tags are fine and that the tag must be pushed.

for a senior

Show how you verify a release rather than assume it: a clean machine with an empty module cache, a throwaway module, one import, one build. Name the failures that catches — unpushed tag, stale module path, private repository, tag ahead of the go.mod commit.

for a principal

Own the release discipline itself: who is allowed to cut a tag, whether releases come from a reviewed commit, and what the team promises about the versions it has published. The technical step is trivial; the commitment it creates is not.

## There is no upload step Most ecosystems have a registry you push a built artifact to. Go does not. A module version is a **tag in a source repository the go command can fetch**, and "publishing" is the act of pushing that tag. Nobody at Google approves it, there is no account, and there is no artifact format you produce. This surprises people arriving from other languages more than any other part of Go modules. Three things have to line up. ### 1. A go.mod whose module path is the import path The first line of `go.mod` declares the module's identity: ``` module github.com/acme/logkit ``` That string is what other people will write in their `import` statements, and it must be a path the go command can turn back into your repository. For the common Git hosts the mapping is built in: the path prefix is the repository. Publish a module whose declared path does not resolve to the repository it lives in and consumers get an error the moment they fetch it. ### 2. A commit that contains everything the release should have A version is a snapshot of a commit's tree. Whatever `go.mod`, source files and `go.sum` exist at that commit are the release. There is no post-publish edit: you cannot amend a version after the fact, and moving the tag later is the single most damaging thing you can do to consumers. So the ordering is always: change the code, run `go mod tidy`, commit, *then* tag. ### 3. A tag whose name is the version ``` git tag v1.4.0 git push origin v1.4.0 ``` Rules that trip people up: - The leading `v` is mandatory. `1.4.0` is invisible to the go command. - The remainder must be valid semantic versioning: three numeric components, optionally a pre-release suffix such as `v1.4.0-rc.1` and build metadata after `+`. - Lightweight and annotated tags both work. Go reads the tag name and the commit it points at; annotation is a team convention, not a requirement. - The tag must be **pushed**. A local tag publishes nothing. - A `v0.x.y` version is a real, fetchable release; by convention it carries no compatibility promise, which is why so many libraries sit at v0 for a long time. ### What the consumer actually does ``` go get github.com/acme/[email protected] ``` The go command resolves the module path to a source of module content, downloads the tagged tree as a module zip, checks that the zip's `go.mod` declares the same path, and then writes two things into the consuming module: a `require github.com/acme/logkit v1.4.0` line in `go.mod`, and cryptographic hashes of the module content and its `go.mod` in `go.sum`. From that point the consumer's build is pinned to those exact bytes. ### Verifying you actually published The laptop you developed on is the worst place to check: it already has the source, and possibly a local replacement or a warm module cache. The honest test is a **clean machine with an empty module cache**, running `go get` for the exact path and version and building a two-line program that imports the package. `go list -m -versions github.com/acme/logkit` is the quick check that the version is visible at all. Common failures this catches: the tag was never pushed; the `module` line still says the old path from before the repository moved; the tagged commit predates the `go.mod` you meant to ship; the repository is private and the consumer cannot reach it. ### Pre-release and repeat publishing Cutting the next release is the same three steps with a higher tag. Patch releases (`v1.4.1`) carry bug fixes, minor releases (`v1.5.0`) add API, and once a version exists you never reuse or move its tag — you always publish forward.

  • Does the tag have to be annotated, or is a lightweight tag enough?
    Either works. The go command only cares about the tag's name and the commit it points at, so a lightweight `git tag v1.4.0` publishes exactly as well as an annotated one. Annotated tags carry a message and an author, which is useful for humans and for release notes, but it is a team convention rather than a module requirement.
  • You tagged v1.2.0 before committing the updated go.mod. What now?
    That version is now permanently the tree you tagged — an old module path, a missing require line, whatever it was. You do not fix it by moving the tag; anyone who already fetched it recorded the original content. You commit the correction and publish v1.2.1, then tell consumers to move up.
  • How do you prove the release is actually consumable?
    On a machine with an empty module cache, create a throwaway module, run `go get [email protected]`, import one package and build. That exercises path resolution, tag visibility and access permissions in one go. `go list -m -versions path` is the faster first check that the version is even visible.

Publishing here is closer to putting a bookmark in a book than to shipping a parcel: you are not sending anything anywhere, you are marking a commit and telling people its name.

saying these in an interview costs you the question

  • Thinks you upload a package to a central registry
  • Omits the leading v from the tag name
  • Tags first, then commits the go.mod change
  • Expects a version field inside go.mod
  • Forgets to push the tag and calls it released
  • Verifies only on the machine that wrote the code