You maintain a shared Go module. A breaking API change is coming. Which repos across your org import it — and at which version?
You own github.com/company/platform-lib. Maybe it's your internal observability library that every service imports for structured logging and trace propagation. Maybe it's a shared HTTP middleware package that standardises authentication, rate limiting, and request IDs across all your APIs. Maybe it's a configuration loader that services use to pull settings from your config management system at startup.
It started as a few shared utilities to avoid copy-pasting. Twenty services adopted it. Then more. Some import it directly and call into it heavily. Some pull it in transitively because another shared library depends on it. Some are on the latest version. Some are three majors behind.
Now you need to change it. Maybe you're removing a deprecated function that's been generating noise in the godoc for two years. Maybe you're bumping to v2 because you need to restructure the package API in a backwards-incompatible way. Maybe you're changing the signature of a middleware constructor to accept an options struct instead of positional arguments.
The question is the same one that runs through every post in this series: which repos across the org import this module, and at which version?
The scenario
Here is what Go module consumption looks like in practice. Your platform team publishes a module, and other repos declare it in their go.mod:
// service-a/go.mod
module github.com/company/service-a
go 1.22
require (
github.com/company/platform-lib v1.8.3
github.com/company/auth-middleware v2.1.0
)
Some repos import the module directly and use it throughout their code. Others pull it in as an indirect dependency — because a library they depend on imports your module — and Go records it with an // indirect comment:
require (
github.com/company/platform-lib v1.8.3
github.com/company/some-other-lib v0.4.1 // indirect
)
Some repos are pinned to a specific release tag. Some reference a pseudo-version pointing to a specific commit:
github.com/company/platform-lib v0.0.0-20260301153042-a1b2c3d4e5f6
Some have overridden the module source entirely with a replace directive — pointing to a local checkout or a patched fork:
replace github.com/company/platform-lib => ../platform-lib
All of these represent consumption of your module. None of them look the same. And right now, they live in different repos, owned by different teams, that you probably can't enumerate off the top of your head.
What existing tools give you (and where they stop)
pkg.go.dev
pkg.go.dev is the official Go package documentation and discovery site. For public modules, it indexes which packages import a given package and shows an importer list. This is the closest existing tool to "who uses this?"
The limitation is sharp: pkg.go.dev only indexes public modules fetched through the public Go module proxy at proxy.golang.org. If your module is hosted on an internal GitLab or GitHub instance, protected by authentication, and routed through a private GOPROXY, it does not appear in pkg.go.dev at all. Every consumer in your org is invisible to it.
For most platform teams managing internal shared libraries, this makes pkg.go.dev entirely useless for the question at hand. Even for public modules, the importer list only covers packages that pkg.go.dev has itself indexed — not a comprehensive count of every organisation that imports the module.
go mod graph
The go mod graph command prints the full module dependency graph for the current module — a recursive list of every direct and transitive dependency. It's useful for understanding what a single module pulls in.
But it only works outward from the current module. There is no go mod reverse-graph. You'd have to clone every repo in the org, run go mod graph in each one, collect the output, and invert it yourself — which is exactly the manual trawl the question is trying to avoid. And the moment you finish, the results are stale.
Renovate and Dependabot
Both Renovate and Dependabot support Go modules as a first-class ecosystem. They detect require entries in go.mod and open pull requests when newer versions are published — which means they implicitly know which repos consume which modules.
But they don't expose that knowledge as a queryable view. You cannot ask Renovate or Dependabot "show me every repo in the org that imports platform-lib, and what version each is on." They react to new versions being published. The reverse — who's consuming the current version before you publish a breaking one — is not something they surface.
Both tools also only cover repos where they've been configured. A team that hasn't set up Go module support in their config, or doesn't use either tool at all, is invisible.
GitHub code search
You can search for your module path across all repos in an org:
org:my-org "github.com/company/platform-lib"
This finds go.mod files that contain the module path and gives you a list of repos. For a one-off audit, it's a workable starting point.
The familiar problems apply: results don't include version numbers without opening each file individually, the search index lags behind recent commits, and it doesn't account for replace directives. If a repo has replaced your module with a fork or local path, the require entry still appears — but what's actually being compiled is different. Code search can't tell you that.
Athens (GOPROXY)
Athens is a widely-used open-source Go module proxy that organisations run internally for private modules and caching. It serves module downloads and maintains a local cache.
Athens tracks which modules were fetched and at which version. What it doesn't track is which source repo triggered the download. A build server fetching platform-lib v1.8.3 is recorded as a download event, but not which team's CI pipeline it was, which service repo it was building, or which go.mod declared the dependency. Athens is a supply-chain cache, not a consumption graph.
Why this is harder than it looks
Major version suffixes create distinct module identities. Go's module system treats github.com/company/platform-lib and github.com/company/platform-lib/v2 as completely separate modules. A repo that imports platform-lib/v2 has a different dependency than one that imports platform-lib. Grepping for platform-lib finds both — but a tool that tracks module identities needs to recognise that these are different versions of the same thing, and that consumers may be on any combination of v1, v2, v3.
This also means publishing a new major version doesn't automatically affect v1 consumers. They continue importing the old module path until they explicitly update both the go.mod entry and all import paths in their source code. Understanding who's still on v1 requires knowing who's importing which module path — and treating those as a unified dependency family.
replace directives obscure the real source. A replace directive can substitute your module with a different source — a local path, a fork, or a different module entirely:
replace github.com/company/platform-lib => github.com/company/platform-lib-patched v1.8.3-hotfix
From the outside, the require entry still references github.com/company/platform-lib. A scanner that reads only require entries sees this as a consumer on v1.8.3. But the code being compiled is coming from a patched fork. The apparent consumer is not actually running your module — and a tool that reports it as one gives you a misleading picture of who's affected by a change to the canonical source.
Indirect dependencies inflate the consumer list. Go's go.mod includes both direct and indirect dependencies, with // indirect annotations on the latter. If your module is a transitive dependency — imported by a library that's imported by a service — it appears in that service's go.mod even though the service's code never directly calls into yours.
This matters when reasoning about impact. A service that directly imports your module and calls its functions will break if you change the API. A service that has it only as an indirect dependency may not be affected at all, depending on how much of your interface the intermediate library exposes. Treating all go.mod entries as equivalent overstates the blast radius; ignoring indirect entries understates it.
go.work workspaces cross module boundaries. Go 1.18 introduced workspace mode, where a go.work file at the root of a checkout declares multiple local modules as part of a unified workspace. When workspace mode is active, dependencies between modules in the go.work are resolved locally rather than through go.mod require declarations.
Some consumers of your module may be developing against it in a shared workspace, with no explicit require entry in their go.mod during development. A scanner that only reads go.mod files misses these entirely.
Vendor directories shadow the dependency graph. Repos that vendor their dependencies with go mod vendor keep a copy of all transitive dependencies under vendor/, with canonical version information in vendor/modules.txt. Some repos modify vendored code directly — technically discouraged, but not uncommon in practice. These modifications break the assumption that a version string in go.mod corresponds exactly to the canonical upstream source. A consumer may appear to be on v1.8.3 while actually running a locally modified version of it.
What the full answer requires
To reliably answer "who imports this module," you need a system that:
-
Scans every repo in the org, parsing
go.modfiles — without requiring opt-in or registration from each team -
Handles all reference forms — tagged versions, pseudo-versions, and
replacedirectives — and records what module source is actually being resolved, not just what appears inrequire -
Tracks all major version suffixes as distinct but related module identities, so you see the full picture across
v1,v2,v3 - Distinguishes direct from indirect dependencies, so you know which consumers call into your module directly and which pull it in transitively through another library
- Follows transitive edges to show second-order consumers: repos that depend on your module through an intermediate shared library
- Keeps results current through regular rescans rather than a one-time snapshot
This is one of the specific problems Riftmap is built to solve. It connects to your GitHub or GitLab organisation, scans every repo, and parses go.mod files to extract module dependencies — including all require entries, replace directives, and version strings. It resolves module paths across major version suffixes and builds a cross-repo dependency graph you can query by module.
The result is the same kind of view described throughout this series: before you remove a function, bump a major version, or change a constructor signature, you open the graph, click the module, and see every repo that imports it — their version, whether they're a direct or indirect consumer, and which teams own them. You know who'll break. You know who to notify. You know who's still on v1 and will need a migration path before v2 ships.
No manual go mod graph inversions across dozens of repos. No org-wide code search with manual version lookups. No waiting to see whose CI goes red after you merge.
How is your team solving this today? I'd genuinely like to know — drop a comment or find me at riftmap.dev.
Top comments (0)