Go Developer Tooling: LSP, Debugger, and Analyzers
Go ships a small language and a large toolchain.
Daily productivity comes from three cooperating layers: a language server for editor intelligence, a debugger for runtime truth, and static analyzers for policy at scale.
Summary
- gopls (the Go language server) answers "what does this code mean?", Delve answers "what is the process doing right now?", and analyzers answer "does this code violate rules we care about before it ships?"
- Insight: Without these tools, Go's simplicity does not save you from slow feedback loops, blind debugging, and review-time surprises about nil safety, security, or dependency CVEs.
- Key Concepts: LSP (Language Server Protocol), gopls, Delve (
dlv), go/analysis, diagnostics, govulncheck, golangci-lint. - When to Use: Onboarding a team, standardizing editor setup, designing CI quality gates, or deciding where a new team rule should live (editor hint vs CI failure).
- Limitations/Trade-offs: gopls and analyzers reason about source, not live heap state; Delve needs debug symbols and slows execution; analyzers can false-positive until tuned.
- Related Topics: Module layout, build tags, test coverage, profiling (
pprof), and production observability.
Foundations
Go intentionally keeps the compiler fast and the language small.
The ecosystem fills the gap with tools that understand Go's type system and package graph.
gopls implements the Language Server Protocol for Go.
Your editor (VS Code, GoLand, Neovim, Emacs) speaks LSP; gopls loads your module, type-checks packages, and serves completions, go-to-definition, rename, and diagnostics.
It is the official language server maintained alongside golang.org/x/tools.
Delve is the de facto Go debugger.
It controls a running process (or core dump), sets breakpoints, steps through goroutines, and inspects variables and stacks.
Unlike printf debugging, Delve shows you actual values at a stopped point, including interface dynamic types and channel state.
Analyzers are programs that walk Go syntax and types looking for bugs or policy violations.
The standard framework is go/analysis: each analyzer declares which facts it needs, runs per package, and reports diagnostics.
go vet, staticcheck, gosec, nilaway, and golangci-lint are all built on or around this model.
Think of the workflow as three clocks running at different speeds:
edit in IDE --> gopls (milliseconds) --> squiggles + refactors
run / test --> Delve (seconds) --> runtime inspection
push / CI --> analyzers (minutes) --> merge gate
Mechanics & Interactions
gopls and analyzers share the same foundation: the type checker from go/types.
When you open a file, gopls builds a package snapshot for your module, respecting go.mod, build tags, and GOOS/GOARCH.
Diagnostics you see in the editor often come from the same analyzers CI runs, but gopls may run a subset for latency and cache results in memory.
Renames and "find references" are not text search.
gopls resolves identifiers through types, so renaming an exported function updates importers across the module.
Delve sits on a different axis.
It requires binaries built with debug information (-gcflags defaults usually suffice).
It uses the DWARF debug data the Go compiler emits and understands goroutine scheduling, so you can list all goroutines, inspect stacks, and break on panic.
Remote debugging attaches Delve to a process in a container or VM while your IDE stays local.
Analyzers compose in CI.
golangci-lint orchestrates dozens of linters with one config file.
govulncheck is separate: it matches your module graph against the Go vulnerability database, not style rules.
// analyzers see typed facts, not just text
import "go/analysis"
var Analyzer = &analysis.Analyzer{
Name: "banfmtprint",
Doc: "disallow fmt.Print in library packages",
Run: run,
}Custom team rules typically become a go/analysis plugin or a golangci-lint custom linter, then run in CI where enforcement is authoritative.
Advanced Considerations & Applications
| Tool layer | Strength | Weakness | Best fit |
|---|---|---|---|
| gopls | Instant feedback, safe refactors | May differ slightly from CI linter set | Daily editing |
| Delve | Ground truth at breakpoints | Overhead, needs reproducible state | Concurrency bugs, unexpected nils |
| CI analyzers | Consistent policy for every PR | Slower, needs baseline for legacy code | Security, nil safety, API rules |
| govulncheck | CVE signal on real import paths | Does not prove exploitability | Dependency hygiene |
Monorepos and workspaces: gopls respects go.work; point editors at the workspace root so cross-module references resolve.
Build tags: gopls and analyzers honor tags in settings or config; mismatched tags between editor and CI produce "works on my machine" diagnostics.
Performance: gopls memory grows with module size; use gopls memory limits and exclude vendor/ from indexing when appropriate.
Security posture: gosec and govulncheck complement each other - one scans idioms in your code, the other scans known flaws in dependencies you actually call.
Common Misconceptions
- "If gopls shows no errors, CI will pass." - gopls runs a curated analyzer set for speed; CI may enable stricter linters, different build tags, or module download in a clean environment.
- "Delve replaces logging." - Delve inspects one process at breakpoints; high-throughput services still need structured logs and traces for production incidents.
- "Analyzers are just style checkers." - staticcheck and nilaway catch real correctness bugs (impossible conditions, nil dereference paths); treat them as test-adjacent quality tools.
- "govulncheck failing means we are exploited." - It reports reachable vulnerable symbols in your graph; triage severity, version bumps, and whether your code calls the vulnerable API.
- "We need a custom linter for every policy." - Start with
go vet, staticcheck, and golangci-lint presets; customgo/analysisanalyzers earn their keep when rules are domain-specific and stable.
FAQs
What is the difference between gopls and the Go compiler?
The compiler (go build) emits binaries and enforces legality.
gopls reuses type-checking logic to serve editor features incrementally and keep a long-lived cache for open files.
Does gopls run the same checks as golangci-lint?
Partially.
gopls embeds some analyzers for diagnostics but not the full golangci-lint bundle.
Treat CI as the source of truth for merge policy.
When should I reach for Delve instead of tests?
Use Delve when state is hard to reproduce in a unit test: race timing, third-party callbacks, or a bug that only appears after many goroutines start.
Tests remain the default for regression prevention.
What is go/analysis and why does everyone mention it?
It is the standard analyzer API in golang.org/x/tools/go/analysis.
It defines how analyzers declare dependencies, share facts, and report diagnostics so tools can compose them reliably.
Can analyzers run inside the editor and in CI?
Yes - that is the ideal setup.
Editors give early feedback; CI enforces the same rules on every push with a clean module cache.
Why does gopls use a lot of CPU after opening a big repo?
It type-checks packages to build the index.
Initial workspace load is heavy; later edits are incremental.
Exclude non-Go trees and tune gopls memory mode if needed.
Is Delve safe to attach to production?
Attaching debuggers to production is risky (pause the world, expose memory).
Prefer staging repros, core dumps, or ephemeral debug pods with strict access controls.
How does govulncheck differ from Dependabot-style alerts?
govulncheck analyzes import paths and call graph reachability in Go modules.
Generic dependency bots may flag packages you import but never expose to vulnerable APIs.
Do I still need go vet if I use golangci-lint?
go vet checks are often included in golangci-lint, but running go vet ./... remains a simple baseline in scripts and CI stages.
Where should team coding rules live?
Encode enforceable rules in analyzers or CI config.
Document human judgment (naming taste, architecture) in review guides, not linters, until the rule is objective enough to automate.
Related
- Go Tooling Setup Basics - install gopls, Delve, and core analyzers
- gopls: Navigation, Refactoring & Diagnostics - IDE features in depth
- Debugging with Delve - breakpoints and goroutine inspection
- staticcheck & golangci-lint in CI - merge-time analyzer policy
- govulncheck for Dependency Vulnerabilities - CVE scanning in modules
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).