Most code vulnerability scanning tools answer "does this dependency have a CVE" — govulncheck answers the sharper question of whether your code can actually reach the vulnerable part of it, which is why it's worth understanding before you wire yet another automated vulnerability scanning tool into CI. Ask two Go vulnerability scanners about the same repository and you'll often get wildly different numbers — one reports forty findings, the other reports three. Both can be right. The difference is reachability: whether the tool checks only that a vulnerable version appears in your dependency graph, or whether it verifies that your code can actually call the vulnerable function. Go is unusually well positioned here because the official tool, govulncheck, does the harder, more useful thing by default. It builds a call graph of your program and reports only vulnerabilities on a path your code genuinely reaches. Understanding that distinction is the difference between a triage queue you can clear and one that trains your team to ignore alerts.
How does govulncheck actually decide what to report?
It analyzes your compiled call graph and matches it against symbol-level vulnerability data, so a CVE is only reported if a vulnerable function or method is reachable from your code.
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
The Go vulnerability database at vuln.go.dev doesn't just say "module X version Y is vulnerable" — for most entries it records the specific affected symbols, e.g. a particular function in net/http or a method in a third-party package. govulncheck then asks: does a path exist from main to that symbol? If yes, it's reported as a call-stack you can inspect. If the vulnerable module is present but the affected function is never invoked on any reachable path, it's downgraded to informational. That's why govulncheck's output is dramatically quieter than a version-matching scanner — and why the findings it does surface deserve immediate attention.
What does a reachable finding look like versus a non-reachable one?
A reachable finding hands you a concrete call stack; a non-reachable one tells you the module is present but unused on any live path.
Vulnerability #1: GO-2025-XXXX
A parsing flaw in example.com/parser allows ...
More info: https://pkg.go.dev/vuln/GO-2025-XXXX
Module: example.com/parser
Found in: example.com/parser@v1.4.0
Fixed in: example.com/parser@v1.4.1
Example traces found:
#1: yourapp/handler.go:42:13: handler.Parse calls parser.Decode
That trace — handler.Parse calls parser.Decode — is the whole value proposition. You're not guessing whether the CVE matters; you can see the line in your code that reaches it. Contrast that with the informational section govulncheck prints for modules that are present but whose vulnerable symbols you never call. Those still warrant a version bump on your own schedule, but they are not the fire.
How is this different from a generic SCA scanner?
Generic scanners match versions; govulncheck matches call paths. Both have a place — and both are still a form of code vulnerability scanning, distinct from web vulnerability scanning (DAST), which probes a running application from the outside rather than analyzing source or a call graph. If your Go service also has a public HTTP surface, govulncheck and a full SCA pass won't catch what a web vulnerability scanning tool would (misconfigurations, auth bypasses reachable only at runtime) — the two approaches answer different questions.
| Dimension | Version-only SCA | govulncheck (reachability) |
|---|---|---|
| Unit of matching | module + version | vulnerable symbol on a call path |
| False-positive rate | high (flags unreachable code) | low |
| Coverage of ecosystems | broad (many languages) | Go only |
| License/inventory data | usually yes | no |
| Catches vulns in unused deps | yes (as noise) | only informationally |
The honest takeaway: govulncheck is the best first filter for "what should a Go developer fix today," but it deliberately doesn't do license compliance, cross-ecosystem inventory, or SBOM generation. Mature programs run govulncheck for prioritization and layer a full software composition analysis pass for inventory, licenses, and continuous monitoring across every service and ecosystem.
How do you wire it into CI without blocking on noise?
Fail the build on reachable findings; report the rest without failing.
# GitHub Actions
- uses: actions/setup-go@v5
with: { go-version: 'stable' }
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
govulncheck exits nonzero when it finds a reachable vulnerability, so the default behavior already gates on the findings that matter. Use the -format json output if you want to route results into a dashboard or apply custom policy (for example, a grace window on medium-severity fixes). A few operational notes:
- Run it against
./...at the module root so the whole call graph is analyzed; scanning a single package misses cross-package paths. - Pin the Go toolchain in CI —
govulncheckalso reports vulnerabilities in the standard library tied to your Go version, so an outdated toolchain shows up as findings you fix by upgrading Go itself. - Cache the module download but not the vuln database; you want the latest advisories each run.
What are the limits you need to plan around?
Reachability analysis is powerful but not omniscient — know where it stops.
govulncheck reasons about Go code it can see. It cannot analyze into cgo/C dependencies, and dynamic dispatch through reflection or plugins can make a truly-reachable path look unreachable (a false negative — rarer but more dangerous than a false positive). It also scans source or binaries you build, so a vulnerability introduced by a build-time tool outside the call graph won't appear. Treat govulncheck as a high-precision core, and cover the edges — build tooling, containers, cross-ecosystem services — with broader scanning. Our Trivy comparison walks through where a generalist scanner complements Go-native analysis.
The scanning checklist
- Run
govulncheck ./...locally and in CI; gate on reachable findings. - Keep the Go toolchain current so stdlib advisories are covered.
- Add a full SCA pass for inventory, licenses, and non-Go components.
- Route JSON output into a policy engine for grace windows and reporting.
- Auto-open remediation PRs so a reachable finding becomes a merge, not a manual chase.
How Safeguard Helps
Safeguard runs reachability-aware scanning on your Go modules and combines it with the inventory, license, and cross-ecosystem coverage govulncheck intentionally leaves out — all from one place. Run it locally or in CI through the Safeguard CLI, let software composition analysis track findings continuously as advisories land, and let auto-fix open a tested pull request with the minimal version bump the moment a reachable vulnerability has a fix. Griffin, the AI analysis engine, explains each call stack in plain language and proposes the change.
Scan your Go modules free at app.safeguard.sh/register, with integration docs at docs.safeguard.sh.