Skip to content
oss-kit
GitHubInstall

Go modules

Go modules are present when a go.mod or a go.sum turns up anywhere in the checkout. Go’s module reference states that “a go.mod file must contain exactly one module directive” and that the directive “defines the main module’s path”, so the manifest always carries the identity the ecosystem resolves by.

Go modules are shipped when that module path resolves publicly and a semantic version tag exists for it. There is no publish command to search a workflow for, because Go has no publish step: a module is published by pushing a vX.Y.Z tag, which proxy.golang.org fetches on demand. The evidence is therefore the tag on the repository the module path names, plus the proxy serving that path at that version.

Source: Go modules reference.

Three cases decide most arguments:

  • A go.mod with no version tag on its repository is present and not shipped. So is one whose module path names a host the code was never pushed to. The absence of a publish step proves nothing either way here, which is what makes this ecosystem different from every registry-push one.
  • From major version 2 onward the shipped identity carries the suffix. Go states that “module paths must have a major version suffix like /v2 that matches the major version”, so example.com/mod and example.com/mod/v2 are two identities, and the one to check the proxy for is the one the current go.mod declares.
  • A go.mod under tools/, examples/, or a test fixture, isolating a build dependency from the main module, is present. A module whose only package is package main is still a module: nobody importing it does not make it unshipped, and a tag on a resolvable path is what decides.

Go modules take the tag-published track. Nothing is uploaded and no publishing credential exists at all, because the registry side is a proxy that reads the forge: pushing the tag is the publish. That is the evidence that assigns the track, and it is why the release area’s preamble drops the four rules that describe an upload. The roster records "track": "tag-published" for Go modules and the preamble names them there.

Verified 2026-07-31 against https://go.dev/ref/mod and https://go.dev/doc/modules/publishing.

Shields.io serves no version badge for a Go module. Its version category carries github/v/tag and gitlab/v/tag and nothing that reads a module proxy, and there is nothing on the other side to read: proxy.golang.org runs a module mirror, a checksum database, and a version index, none of which serves a badge. pkg.go.dev does publish a badge, the Go Reference one, and that badge links to the package documentation rather than reporting a version, so the live-fact test in badges.md keeps it out of the row.

Source: Go Module Mirror, Index, and Checksum Database and pkg.go.dev badge.

The strongest documented fallback is the tag badge, which reports the same artifact the proxy resolves:

[![version](https://img.shields.io/github/v/tag/OWNER/REPO?sort=semver&label=version)](https://pkg.go.dev/github.com/OWNER/REPO)

Neither query parameter is optional. sort defaults to date, which returns whichever tag was committed most recently rather than the highest version, so a repository that backports a patch to an old line reports the old line as current. And the default label is tag, which names a git artifact; label=version is what a reader needs, because a tag is the mechanism here and the version is the fact.

On GitLab the same badge is img.shields.io/gitlab/v/tag/NAMESPACE/PROJECT?sort=semver&label=version, which takes the full project path in one parameter and a gitlab_url parameter for a self-managed instance.

This sits below what a registry badge gives. A tag exists whether or not the proxy ever served that version, so the badge can state a version that go get will not resolve, and R-DOC-04 is what that risks. A version endpoint on proxy.golang.org, or a shields.io service reading index.golang.org, would retire the gap.

go get MODULE

That is the line for a module a reader imports. A module whose point is an executable shows go install MODULE@latest instead, which Go documents as building in module-aware mode and ignoring any go.mod in the current directory, so it installs the tool without touching the reader’s own dependencies.

Take MODULE from the module line of go.mod rather than from the repository URL. At major version 2 and above the two differ: the path carries a /v2 suffix that the clone URL does not, and a README that drops it names a module that does not exist.

Verified 2026-07-31 against Managing dependencies, Go Modules Reference, Go Module Mirror, Index, and Checksum Database, pkg.go.dev badge, GitHub Tag badge, and services/github/github-tag.service.js plus services/gitlab/gitlab-tag.service.js and services/github/github-common-release.js in badges/shields.

go.mod has no license field. The module reference’s file syntax section names ten directives, module, go, toolchain, godebug, require, tool, ignore, exclude, replace, and retract, and none of them carries package metadata of any kind. There is no author, no description, and no license, because a module is published by pushing a tag rather than by uploading a metadata document.

What stands in its place is file detection. pkg.go.dev runs github.com/google/licensecheck over files whose names it matches case-insensitively against a fixed list, LICENSE, LICENCE, COPYING, UNLICENSE, and their .md, .markdown, and .txt variants plus a handful of license-specific names such as LICENSE-APACHE and MIT-LICENSE. It recognizes a fixed set of licenses, all of them OSI-approved. So the license a Go consumer sees is read out of the file itself, not declared anywhere.

That collapses R-COM-01 to one side. There is no manifest value to compare the file against, so what remains is the file existing at the repository root and containing the full text of a license you can name. Read it rather than trusting its filename. A repository shipping a Go module alongside another ecosystem’s manifest, a package.json that builds its documentation for instance, still has that manifest’s declaration to compare, and the comparison is with the same file.

Source: Go modules reference and License policy.

Tidelift lists no platform for this ecosystem, so there is no tidelift value a Go module can write.

GitHub’s own documentation for the funding file enumerates the accepted tidelift platform names. It names six, every one of them another ecosystem’s registry; Go appears nowhere in it. That is why the roster at skills/oss-audit/ecosystems.json carries no tidelift key for this ecosystem.

Source: Displaying a sponsor button in your repository.

The strongest documented fallback is the rest of the same file. The github, open_collective, liberapay, and custom keys are forge-level rather than ecosystem-level, so they reach a Go project exactly as they reach any other. Nothing else is available on this side: go.mod has no funding directive any more than it has a license one, and the module proxy serves no project metadata a funding link could live in.

No rule in STANDARD.md requires a funding file at all, so this gap costs a repository nothing when it is scored. Tidelift adding a Go platform, and GitHub’s funding documentation listing it, is what would retire it.

Verified 2026-07-31 against Go modules reference, License policy, and Displaying a sponsor button in your repository.

On GitHub Actions, actions/setup-go installs a Go version and puts it on the PATH. The support claim lives in the go directive in go.mod, which sets the minimum Go version required to use the module; since Go 1.21 that is a hard requirement rather than advice, and a toolchain older than the directive refuses to build the module at all.

strategy:
matrix:
go-version: ['1.24', '1.25']
steps:
- uses: actions/setup-go@v7 # oss-harden pins this to a commit SHA
with:
go-version: ${{ matrix.go-version }}

go-version-file: go.mod reads the directive instead of repeating it, which keeps one version in one place; from v6 the action prefers the toolchain directive when the file carries one and falls back to go. That form pins a single version rather than a matrix, so use it for a single-version pipeline and the explicit list above for a matrix.

Go’s own release policy bounds how many lines there are to cover: each major release is supported until two newer major releases exist. A module whose go directive names an unsupported version still claims it, so the matrix covers the directive’s version and every supported line above it.

On GitLab, run the job in the Go official image and vary the tag with parallel:matrix.

Sources: actions/setup-go, Go modules reference, Go release policy, docker-library/golang.

actions/setup-go caches Go modules and build outputs, and unlike the other setup actions its caching is on by default; cache: false turns it off.

Read the key before trusting it. From v6 the action’s default cache key hashes go.mod, not go.sum, and the README says to set cache-dependency-path: go.sum to key on the sum file instead. go.mod records the module requirements while go.sum records the resolved content hashes, so the default key survives a dependency change that only go.sum records.

- uses: actions/setup-go@v7 # oss-harden pins this to a commit SHA
with:
go-version: ${{ matrix.go-version }}
cache-dependency-path: go.sum

The module cache itself lives at $GOMODCACHE, defaulting to $GOPATH/pkg/mod, and it is download data by construction: the go command stores versioned, verified copies there. On GitLab set GOMODCACHE to a project-relative directory and key cache:key:files on go.sum, because GitLab caches only paths inside the project directory.

Sources: actions/setup-go, Go modules reference, module cache, GitLab CI/CD YAML reference, cache.

go test ./... is the invocation, and it is what Go’s own publishing guide runs before tagging a release. Go declares the suite by layout rather than in a manifest: the go command discovers _test.go files in every package the pattern matches, so ./... covers the module and no configuration file states it.

That means go.mod carrying no test entry says nothing about whether a suite exists. Look for _test.go files, and treat a module with executable code and none as the gap.

Sources: Publishing a module.

Verified 2026-07-31 against https://github.com/actions/setup-go, https://go.dev/ref/mod, https://go.dev/doc/devel/release, https://go.dev/doc/modules/publishing, https://github.com/docker-library/golang, and https://docs.gitlab.com/ci/yaml/.

Dependabot’s gomod value carries version updates and security updates, and vendoring is supported, so a project with a vendor/ directory still gets both. Private registries are not supported for this value.

On GitLab the Renovate manager is gomod.

Go splits what other ecosystems put in one file. go.mod fixes the selected versions, and go.sum records the hashes, so both halves need reading before this is reported either way.

The freeze is the default rather than a flag. Since Go 1.16 the go command acts as if -mod=readonly were set: it reports an error and suggests a fix if go.mod needs updating, rather than editing it. A project with a vendor/ directory and a go directive of 1.14 or higher gets -mod=vendor instead, which is a different guarantee, so read the directive before assuming which applies. Setting -mod=readonly explicitly in CI is worth doing for the same reason a permissions: block is written down: it makes the behaviour visible in the file rather than derived from a toolchain version.

go mod verify is the integrity half. It hashes each downloaded module zip and extracted directory in the module cache, compares them against the hash recorded when the module was first downloaded, prints “all modules verified” on success, and exits non-zero naming what changed. Every module-aware command already checks the cache against go.sum, so go mod verify in CI is a deliberate re-check rather than the only one.

What breaks the first time you commit go.mod and go.sum

Section titled “What breaks the first time you commit go.mod and go.sum”

The matrix floor is the go directive, and a dependency update can raise it. Before Go 1.21 the directive was advisory; it is now a requirement, and “Go toolchains refuse to use modules declaring newer Go versions”. Worse for an updater pull request, the go command “writes its own toolchain name in a toolchain line any time it is updating the go version in the go.mod file (usually during go get)”. So a routine bump can raise the floor in the same commit that raises the dependency, and every job below the new floor stops before compiling. Read the diff for a changed go or toolchain line before merging one.

go.sum is pruned against the previous release, and -compat moves that. go mod tidy records “checksums needed by the Go version one below the version specified in its go directive”, and -compat overrides which version that is. A matrix reaching two releases below the directive therefore needs -compat set to its oldest leg, or that leg fails on a missing checksum rather than on a missing module.

Platform is not a dimension here. Build constraints other than ignore “are not considered” by go mod tidy and the other module commands, so a module imported only under one GOOS stays in go.mod and go.sum whatever machine ran tidy. There is no per-platform regeneration to arrange and no equivalent of adding a platform to the lock.

Verified 2026-07-31 against Go modules reference and go mod tidy.

CodeQL supports Go, so CodeQL default setup covers this ecosystem on GitHub. GitLab’s SAST table lists Go under its Semgrep-based analyzer with GitLab-managed rules.

The dependency graph parses go.mod and nothing else; go.sum is not in its table. The Go modules row does support Dependabot graph jobs, which is how the resolved set reaches the graph despite only the manifest being parsed.

Advisories come from the GitHub Advisory Database, which names this ecosystem Go against the pkg.go.dev registry.

Go also publishes a database of its own at https://vuln.go.dev, curated by the Go security team from the National Vulnerability Database, the GitHub Advisory Database, and reports from Go package maintainers, in OSV format. govulncheck, installed with go install golang.org/x/vuln/cmd/govulncheck@latest and run as govulncheck ./..., reads it and “only surfaces vulnerabilities that actually affect you, based on which functions in your code are transitively calling vulnerable functions”. That reachability filter is a different question from the one a version-matching scanner answers, so the two are complementary: govulncheck says whether this code reaches the vulnerable symbol, and osv-scanner, which reads go.mod, says whether the version is affected at all.

Verified 2026-07-31 against Dependabot supported ecosystems and repositories, Dependency graph supported package ecosystems, GitHub Advisory Database, Go Modules Reference, Go vulnerability management, CodeQL supported languages and frameworks, GitLab SAST, Renovate managers, and osv-scanner supported languages and lockfiles.

Concrete flow for the decisions SKILL.md makes, for a Go module. Go modules run on the tag-published track: there is no registry account, no publish endpoint, and no credential of any kind. A version is published by pushing a git tag, which proxy.golang.org fetches on demand the first time somebody asks for it. STANDARD.md’s release preamble places R-PUB-01 through R-PUB-04 outside that track as a whole, so four of the six sections below record why the step does not exist here rather than how to configure it. What stands where the approval gate would stand is R-SEC-13’s restriction on who may create a tag, because on this track creating the tag is the publish.

Source: Go, Publishing a module, proxy.golang.org, and Go Modules Reference.

Read go.mod. The module line is the module path and the whole of the package identity, including any major version suffix such as /v2; R-CHG-07 covers keeping that suffix and the released major in step, and oss-changelog owns it. Get the owner and repository from that path when it names the forge host directly, falling back to git remote get-url origin.

A module path whose host is not the repository host is served through a vanity import path, which the module reference resolves by fetching an HTML document from that host carrying <meta name="go-import" content="root-path vcs repo-url">. Treat that host as part of the release surface: whoever serves that document decides which repository the module resolves to, and no forge control reaches it.

Check whether a version already exists, which is also the command that prompts the index to notice a new one:

GOPROXY=proxy.golang.org go list -m <module path>@<version>

@latest in place of the version reports the newest version the index knows about. A module that has never been fetched is absent from the proxy rather than unpublished; the tag is what exists, and the proxy is a cache in front of it.

Configure trusted publishing (Step 2): no credential exists to configure

Section titled “Configure trusted publishing (Step 2): no credential exists to configure”

There is nothing to configure and nothing to store. The publishing guide’s whole procedure is go mod tidy, go test ./..., git tag, git push origin <tag>, and the go list -m call above to prompt the index. The three services at proxy.golang.org, the module mirror, sum.golang.org, and index.golang.org, offer no registration, no account, and no ownership record; fetching is demand-triggered.

Source: proxy.golang.org.

R-PUB-02 is therefore not reached rather than met, and neither is R-PUB-07: that rule asks where the credential behind a registry entry update lives, and its own Check: places an ecosystem with no registry entry to update outside it. Report both that way rather than as passes. Anything a Go release workflow does hold a token for, such as attaching binaries to a forge release, is the forge’s own token and belongs to Step 6.

The one thing that behaves like a publishing credential here is push access to a tag. That is R-SEC-13 and oss-harden configures it.

Write the hardened release workflow (Step 3)

Section titled “Write the hardened release workflow (Step 3)”

There is no publish job, so the workflow’s job is to make the tag safe to create rather than to upload anything. Run these checks on every change, not only at release time, because once the tag is pushed the version is fixed for good.

This is the one departure from the workflow shape SKILL.md states, and it is the tag-published track’s, not this ecosystem’s. The jobs below stay in .github/workflows/ci.yml under name: CI, with no Release workflow and no release environment, because pushing the tag is the publish and there is nothing left for a separate workflow to do. test and build keep their names, and the asset job is still github-release.

name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false
- run: go mod tidy && git diff --exit-code go.mod go.sum
- run: go vet ./...
- run: go test ./... # oss-ci decides the actual command from CONTRIBUTING.md (R-CI-02)

The go mod tidy check is the one that matters most at release time: go.sum is what a consumer’s build verifies against, and a tag pushed with a stale go.sum is not fixable afterwards. oss-ci owns what else runs here and oss-harden pins each uses: line to a commit SHA.

The release itself is two commands on the maintainer’s machine, or a job triggered by whatever creates the tag:

git tag v1.2.3
git push origin v1.2.3
GOPROXY=proxy.golang.org go list -m <module path>@v1.2.3

Never move or delete a tag after pushing it. The publishing guide states the consequence plainly: change a tagged version after publishing and Go tools return a security error, because the checksum database has already recorded the bits that version resolved to. The proxy makes it worse in the other direction, since a bad release can stay served from the mirror after the repository is deleted. R-SEC-13 is the rule, and it is a security rule rather than a release rule for exactly this reason.

If a workflow creates the tag, give it a token scoped to that and nothing else, and keep the version check that every other ecosystem runs before publishing: compare the tag against whatever the repository treats as the declared version, since Go itself declares no version anywhere in go.mod.

Gate on manual approval (Step 4): the gate is on tag creation

Section titled “Gate on manual approval (Step 4): the gate is on tag creation”

R-PUB-04 is not applicable on the tag-published track, because there is no run to approve between building and public availability. Pushing the tag is the publish, and the proxy serves it as soon as anybody asks.

That is why R-SEC-13 carries a tag-creation clause. Restrict refs/tags/* on GitHub with a repository ruleset that blocks tag update and tag deletion and restricts tag creation to named principals, or on GitLab with protected tags covering the release tag pattern. oss-harden writes both. Report R-PUB-04 as not applicable and R-SEC-13 as the control standing in its place, rather than reporting an approval gate that is not there.

Verify provenance (Step 5): a gap, not a check

Section titled “Verify provenance (Step 5): a gap, not a check”

Nothing in the Go module ecosystem serves build provenance, and the thing that looks closest is not it. sum.golang.org is a checksum database: it records the hashes a module version resolved to and guarantees that those bits do not change from one day to the next, even if the author later alters the tags. That is integrity for a version, and it says nothing about who built the code or which workflow produced it. There is no build to attest in the first place, because the consumer’s own toolchain compiles the source.

Source: Go Modules Reference and Go, Publishing a module.

The strongest available substitute is the pair the ecosystem does offer. A consumer can verify the downloaded module cache against the checksum database with go mod verify, and the repository can sign its release tags so the commit the version resolves to carries an identity. Tag signing is R-SEC-05 and oss-harden owns it.

This sits below R-PUB-03, which asks for provenance tied to the repository and the workflow that built the artifact. It stays below it while the published artifact is source that the consumer builds, and it would retire the day Go’s toolchain or the mirror served an attestation for a module version. Compiled binaries a Go project attaches to a forge release are a different artifact and Step 6 covers them.

Describe and sign what the release attaches (Step 6)

Section titled “Describe and sign what the release attaches (Step 6)”

Only for a release that attaches a built asset to the forge release. Publishing the module attaches nothing, and the source archives the forge generates for a tag are not built assets, so a library goes straight to Step 7. A Go project shipping a command-line tool commonly attaches compiled binaries, and those are what this section covers.

Go ships its own bill of materials, so no third-party tool enters the release workflow. go version -m <binary> reads the module information the linker embedded, and go mod metadata is in the binary whether or not anybody asks for it. For a release asset, produce the inventory in the build job beside the binary it describes:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false
- run: mkdir -p dist && go build -trimpath -o dist/ ./cmd/...
- run: go version -m dist/* > dist/SBOM.txt
- uses: actions/upload-artifact@v7
with:
name: binaries
path: dist/
retention-days: 1

go version -m output is not SPDX or CycloneDX, so it does not satisfy R-PUB-05 on its own; publish it as the human-readable inventory and say plainly that the rule’s format requirement is unmet until an SBOM generator the maintainer has vetted produces one of the two formats. -trimpath keeps absolute build paths out of the binary, which is what makes two builds of the same commit comparable.

Attach and attest in a separate job, which is the same job boundary the other ecosystems use, for the same reason: a job that can write release assets should not be the job that builds them.

github-release:
runs-on: ubuntu-latest
needs: [build]
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
steps:
- uses: actions/download-artifact@v8
with:
name: binaries
path: dist/
- run: (cd dist && sha256sum *) > SHA256SUMS
- uses: actions/attest@v4
with:
subject-checksums: SHA256SUMS
- run: gh release upload "$GITHUB_REF_NAME" dist/* SHA256SUMS
env:
GH_TOKEN: ${{ github.token }}

sha256sum runs from inside dist/ so the names it writes are the names the assets carry on the release. subject-checksums makes every file in the manifest a subject of the attestation in its own right, by name and digest; attesting SHA256SUMS itself with subject-path would leave a consumer able to verify the manifest and nothing about the assets it lists.

A consumer verifies an asset, then checks the rest of the download against the manifest:

gh attestation verify <asset> --repo <owner>/<repo>
sha256sum -c SHA256SUMS

Run the first command against each asset downloaded, never against SHA256SUMS, which is a subject of nothing. --signer-workflow <owner>/<repo>/.github/workflows/release.yml pins which workflow the attestation must have come from. On GitLab CI/CD the forge attestation above is unavailable, so a GitLab release can carry the same SHA256SUMS with nothing signing it; say that rather than presenting the file as provenance.

gh release upload fails when no release exists for the tag. Either create it in this job with gh release create "$GITHUB_REF_NAME" --generate-notes first, or have the maintainer publish the release from the tag. This reference does not choose between them, because release notes are the project’s own.

A published version cannot be unpublished; deleting the tag does not remove it from the mirror. The documented remedy is the retract directive: add it to go.mod and publish a new version containing it, higher than every other release or pre-release. Retracted versions stay available so existing builds keep working, drop out of @latest and range queries, hide from go list -m -versions unless -retracted is passed, and surface to users on go list -m -u. oss-changelog owns how that gets recorded, under R-CHG-01.

Step 7 is in SKILL.md: read each R-PUB rule’s Check: line against what this file produced, and fix what fails before reporting done.

Verified 2026-07-31 against Go, Publishing a module, Go Modules Reference, and proxy.golang.org.

No file in a Go repository states the release version. go.mod carries a module directive defining the module path and a go directive setting the minimum Go version the module was written for; neither is the release number. The tag is the version, and go.sum records dependency hashes rather than anything about this module.

So the comparison reduces to the tag and the changelog, plus one manifest fact that has to move with a major: the /vN suffix on the module line, covered in the next section but one.

A repository holding several modules tags each one with its subdirectory as a prefix. The reference gives golang.org/x/tools/gopls, which lives in the gopls subdirectory of the repository rooted at golang.org/x/tools, and states that the module subdirectory also serves as a prefix for semantic version tags, so that module’s tags read gopls/vX.Y.Z. Each such module is its own release unit with its own tag prefix and, in most repositories, its own changelog section.

A canonical Go version starts with the letter v followed by a semantic version: three non-negative integers separated by dots, an optional pre-release string after a hyphen, and an optional build metadata string after a plus. v0.0.0, v1.12.134, v8.0.5-pre, and v2.0.9+meta are all valid. The v is not optional here, which is the reverse of every other ecosystem on this roster, and it is the case R-CHG-02’s tag-name allowance exists for.

Build metadata is ignored for comparison, and the go command converts a version carrying it into a pseudo-version to keep the ordering total. Two suffixes are special and are not build metadata in that sense: +incompatible, which the go command adds to a version at major 2 or higher published without a go.mod file, and +dirty. +incompatible should never appear on a tag; it appears only in versions the go command computes.

A pseudo-version is what a consumer gets for an untagged revision, built from a base version prefix, a UTC timestamp yyyymmddhhmmss, and a twelve-character commit hash prefix. It takes one of three forms, vX.0.0-yyyymmddhhmmss-abcdefabcdef with no known base version, vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef after a pre-release, and vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef after a release. None of those is something a maintainer writes, and seeing one in a consumer’s go.mod means the module was never tagged at that point rather than that somebody chose an odd version.

Major version in package identity (R-CHG-07)

Section titled “Major version in package identity (R-CHG-07)”

Go is the one ecosystem on this roster that does encode the major version in package identity, and it is the case R-CHG-07 exists for. The reference states it directly: “If the module is released at major version 2 or higher, the module path must end with a major version suffix like /v2”, and “Major version suffixes are not allowed at major versions v0 or v1”. A module at example.com/mod on v1.0.0 must be example.com/mod/v2 at v2.0.0. The suffix must not have a leading zero, must not be /v1, and must not contain a dot.

Releasing a new major is therefore a source change, not just a tag. The module line changes, every import path inside the module that names the module path changes with it, and the tag follows. A repository tagged v2.0.0 whose go.mod still names the v1 path has published something no consumer of v2 can import, which is why R-CHG-03’s agreement on one number does not catch this.

gopkg.in paths are the exception in the other direction: they always carry a suffix, at every major including v0 and v1, and separate it with a dot rather than a slash, as in gopkg.in/yaml.v2.

Deleting the tag does not withdraw anything. The checksum database ensures the bits for a version do not change from one day to the next even if the author later alters the tags, and the reference is explicit that “this bad release may still be available in the mirror even if it is not available at the origin. The same situation applies if you delete your entire repository.” Withdrawal here is an act of publishing rather than an act of removal.

The mechanism is the retract directive. Add it to go.mod and publish a new version containing it, higher than every other release and pre-release of the module, since a consumer only learns of a retraction from a version they can see. Retracted versions stay available so existing builds keep working, are excluded from @latest and from range queries, are hidden from go list -m -versions unless -retracted is passed, and are reported to users who run go list -m -u or update a related module. A version that contains retractions may retract itself, and where the highest version does that, @latest resolves to a lower one.

That gives the changelog two entries rather than one. Mark the withdrawn version’s heading [YANKED] and keep it, and give the retracting version its own entry saying which versions it retracts and why, because that version is a real release users will see.

Verified 2026-07-31 against Go modules reference and Publishing a module.