gopls & Analysis Tooling Best Practices
Editor intelligence and CI analyzers should tell the same story: fast feedback while typing, authoritative checks before merge.
These practices keep gopls, Delve, and linters aligned across laptops and pipelines.
How to Use This List
- Apply when onboarding engineers and when CI lint jobs disagree with local editors.
- Treat checkbox items as team policy - wire enforceable ones into
.golangci.ymland CI YAML. - Revisit after Go minor upgrades; toolchain and analyzer defaults evolve.
- Pair this list with the section articles for install steps and deep dives.
A - Editor and gopls hygiene
- Open the module or
go.workroot in the editor, not a parent folder. gopls needsgo.mod(or workspace) at the workspace root for correct imports and diagnostics. - Keep
goplsandgoversions current within team policy. Mismatches produce spurious errors or missing features. - Mirror CI build tags in gopls settings. Use
buildFlagsorGOFLAGSso tagged files type-check the same locally and in pipelines. - Enable format on save with
gofumptwhen the team standardizes on it. Consistent formatting removes noise from diffs and review. - Restart gopls after large branch switches or vendor updates. Stale snapshots cause phantom diagnostics until the language server reloads.
B - Debugger practices
- Install Delve (
dlv) alongside gopls for every backend engineer. Debugging goroutines is a core skill, not an escalation-only tool. - Use
dlv testfor failing tests before adding println debugging. Break on the assertion line and inspect goroutine stacks. - Build debug binaries without strip flags when you need variables in Delve. Avoid
-ldflags="-s -w"on images you attach to. - Never expose Delve ports on public networks. Headless debug listens grant memory and execution control - tunnel via SSH or port-forward with access controls.
- Record repro steps when a Delve session finds the bug. Convert the insight into a regression test so the issue does not return silently.
C - CI analyzer policy
- Commit
.golangci.ymlat the repo root and run the same config locally. Drift between laptop and CI erodes trust in lint jobs. - Run
go test ./...,go vet ./..., andgovulncheck ./...on every PR. Tests catch behavior; vet and govulncheck catch distinct failure classes. - Adopt new linters with baselines (
new-from-rev) on brownfield repos. Zero-tolerance day one causes teams to disable CI. - Pin golangci-lint version in CI and document the install command. Reproducible lint results matter for audits and bisects.
- Exclude generated code (
*.pb.go, vendor) from lint, not hand-written packages. Generated noise trains people to ignore findings.
D - Security and nil safety
- Run govulncheck in CI after module download. Upgrade vulnerable modules before release tags, not after incident response.
- Enable gosec on services handling SQL, subprocesses, or user-controlled paths. Tune severity per repo rather than disabling the linter.
- Introduce nilaway (or equivalent) on packages with frequent nil panics. Start with RPC handlers and config loaders.
- Avoid blanket
//nolintcomments. Require linter name, ticket, and reviewer approval for exceptions. - Treat analyzer green as necessary, not sufficient. Pair static analysis with code review and integration tests.
E - Custom rules and ownership
- Encode repeated review comments as
go/analysisanalyzers only when objective. Subjective taste stays in the review guide until it is machine-checkable. - Test custom analyzers with
analysistestbefore enabling in CI. Untested linters create false-positive churn. - Own analyzer modules in
tools/with the same review bar as production code. Linters are part of the product's quality surface. - Document how to run the full local gate (
test,vet,golangci-lint,govulncheck). OneMakefileormagetarget beats tribal knowledge. - Review tooling quarterly: drop unused linters, add rules for new incident classes. Tooling debt accumulates like dependency debt.
FAQs
Should gopls diagnostics match CI exactly?
They should overlap on compiler and high-value checks.
CI may run more analyzers - that is expected.
Align configs to minimize surprises.
What is the minimum CI lint set for a new Go service?
go test, go vet, staticcheck (via golangci-lint), and govulncheck.
Add gosec and nilaway as the threat model requires.
How do I handle legacy lint debt?
Use issues.new-from-rev and fix packages incrementally.
Track burn-down like any other engineering chore.
Is Delve required if we only write CLIs?
CLIs still deadlock, mis-handle errors, and panic.
Delve remains valuable for any non-trivial concurrency or state.
When should we write a custom analyzer?
When the same objective violation appears on many PRs and no upstream linter fits.
Keep the rule narrow and tested.
Should analyzers run on `main` only?
Run on every PR branch.
Main-only scans let vulnerabilities land before the first nightly.
How do monorepos fit?
Lint each module from its go.mod root or document a meta-script.
Share one .golangci.yml template across services where policies match.
Can we skip govulncheck if Dependabot is enabled?
Dependabot lacks Go-specific reachability.
Keep govulncheck as the authoritative Go module CVE gate.
What about editor choice?
Any LSP client with gopls is fine.
Standardize settings and PATH, not necessarily the IDE brand.
How do we onboard contractors quickly?
Point them to Go Tooling Setup Basics, commit editor settings samples, and provide a one-command make check.
Related
- Go Tooling Setup Basics - install and first commands
- gopls: Navigation, Refactoring & Diagnostics - IDE features
- staticcheck & golangci-lint in CI - CI configuration
- govulncheck for Dependency Vulnerabilities - dependency CVEs
- Debugging with Delve - runtime inspection
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).