Go Developer Tooling: LSP, Debugger, and Analyzers
Go ships a small language and a large toolchain.
Search across all documentation pages
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.
dlv), go/analysis, diagnostics, govulncheck, golangci-lint.pprof), and production observability.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
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.
| 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.
go vet, staticcheck, and golangci-lint presets; custom go/analysis analyzers earn their keep when rules are domain-specific and stable.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.
Partially.
gopls embeds some analyzers for diagnostics but not the full golangci-lint bundle.
Treat CI as the source of truth for merge policy.
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.
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.
Yes - that is the ideal setup.
Editors give early feedback; CI enforces the same rules on every push with a clean module cache.
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.
Attaching debuggers to production is risky (pause the world, expose memory).
Prefer staging repros, core dumps, or ephemeral debug pods with strict access controls.
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.
go vet checks are often included in golangci-lint, but running go vet ./... remains a simple baseline in scripts and CI stages.
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.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026