Go's tooling made dependency management refreshingly boring: go.mod declares your requirements, go.sum locks the exact content hashes, and the module graph is deterministic. But boring does not mean safe. A vulnerable version of a transitively imported module still compiles cleanly into your binary, and Go's static linking means whatever ships in that binary is genuinely present at runtime. The Go team's answer to auditing this is govulncheck, and it is worth studying because it does something most audit tools do not.
What makes govulncheck different
Most dependency auditors answer a coarse question: "is a version with a known CVE present in my dependency list?" govulncheck answers a sharper one: "does my program actually call the vulnerable code?" It does this with static analysis. It builds the call graph of your program, cross-references it against the Go vulnerability database (maintained at vuln.go.dev, drawing on the broader OSV ecosystem), and reports vulnerabilities primarily when there is a call path from your code to a vulnerable symbol. Findings where the vulnerable package is imported but never reached are reported at a lower confidence, clearly separated.
That is reachability analysis baked into the native tool, and it dramatically cuts noise. Install and run it against your whole module:
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
The output leads with the vulnerabilities your code can actually trigger, showing the specific call stack from your function down to the vulnerable one. For a fuller picture, including imported-but-unreached packages, raise the verbosity:
govulncheck -show verbose ./...
Auditing binaries and CI integration
A powerful trick: govulncheck can analyze an already-built binary, which is invaluable when you receive artifacts you did not compile or want to verify what actually shipped:
govulncheck -mode=binary ./path/to/service-binary
Binary mode can only report which vulnerable symbols are present in the compiled artifact — it lacks the full source call graph — but it is a strong last-mile check. For pipelines, emit JSON so a downstream system can parse and gate on it:
govulncheck -format json ./... > govulncheck.json
A clean CI check is simply to run govulncheck ./... and fail the job when it reports a reachable vulnerability, treating unreached findings as informational.
Reading the output correctly
The most common mistake teams make with govulncheck is treating a clean summary line as a clean bill of health without reading the detail. govulncheck separates its findings into two tiers: vulnerabilities with a confirmed call path from your code, and vulnerabilities whose module is present but not reached. Both matter, but for different reasons. The reachable set is your immediate work queue. The unreached set is a watchlist — because a refactor that starts calling a previously dormant code path can promote an unreached vulnerability into a reachable one overnight, without any dependency changing at all. Track both, and re-run govulncheck on every meaningful code change rather than only when go.mod changes, precisely because reachability is a property of your code, not just your dependency list.
Where govulncheck stops
For all its strengths, govulncheck is deliberately narrow.
- Go only. It audits Go modules and nothing else. A polyglot repository — Go service, TypeScript frontend, Python data jobs — needs a separate tool per ecosystem, each with its own report format and its own triage.
- Standard-library and known-symbol focus. Its precision depends on the Go vulnerability database's curated advisories with symbol-level data. Advisories without that granularity fall back to coarser matching.
- No policy or history. Like the other native auditors, each run is a snapshot. There is no SLA tracking, no persistent risk-acceptance record, no shared suppression state, and no consolidated view across services.
- No remediation. It tells you the safe version exists; bumping
go.mod, runninggo mod tidy, and validating the build is on you.
Going further across your whole stack
The reachability idea govulncheck pioneered for Go is exactly what you want for every language you ship — and that is the gap a platform fills. Safeguard's software composition analysis engine applies call-graph reachability across ecosystems, so your Go modules, npm packages, and Python wheels are all triaged on the same exploitability-first basis, in one queue, under one policy.
For the deeper "should I actually worry about this" question, the Griffin AI analysis engine explains a reachable Go finding in context — the call path, the exploit conditions, and whether the recommended module bump is likely to be a clean upgrade or a breaking one. And you can keep govulncheck's fast, local feel by running unified scanning through the Safeguard CLI, which slots into the same go test-style loop developers already run.
If you are comparing approaches to reachability-based scanning across vendors, the product comparison hub lays out how the native-tool experience maps onto a continuous platform.
Audit checklist for Go teams
- Commit
go.modandgo.sum; audits are only reproducible when the graph is locked. - Run
govulncheck ./...in CI and fail on reachable findings. - Use
-mode=binaryto verify third-party or released artifacts. - Treat unreached findings as a backlog to review, not to ignore forever — reachability can change as your code evolves.
- Unify Go findings with the rest of your stack so cross-language services get one consistent, prioritized view.
govulncheck is the best-designed native audit tool in any mainstream ecosystem precisely because it leads with reachability. Extend that same discipline across every language and add persistent policy and autonomous fixes, and dependency auditing stops being a periodic fire drill.
Bring reachability-first auditing to every service you run — create a free account or dig into the documentation.