Toolchain and Static Analysis
For developers with .NET (C#) experience, this guide explains developer tools such as gofmt, go vet, golangci-lint, and go generate. The roles played in C# by Roslyn Analyzers, .editorconfig, dotnet format, and Source Generators are split in Go between standard toolchain commands and a small number of external tools. Go culture values leaving "nothing to argue about" with few settings, so this guide covers the commands to learn first and how to lock them in with CI.
1. What This Document Covers
- Non-negotiable formatting with gofmt
- Static analysis with go vet and golangci-lint, and the v2 configuration
- When to use go generate
- Integrating into CI
Non-negotiable formatting with gofmt
gofmt decides the single correct format for Go code. Indentation uses tabs, and the tool decides where braces and spaces go, so style comments in reviews and configuration debates within a team almost never happen.
gofmt -l .lists files that need formatting.gofmt -d .shows the diff, andgofmt -w .rewrites the files.gofmt -salso applies safe simplifications, such as turning[]T{T{...}}into[]T{{...}}.goimports(golang.org/x/tools/cmd/goimports) does everythinggofmtdoes and also adds, removes, and groups imports. Most editors can run equivalent processing on save throughgopls.gofumptis a stricter formatter that adds more rules while staying compatible withgofmt. Adopting it is optional, and it can be enabled as a formatter ingolangci-lint.
Static analysis with go vet and golangci-lint
go vet is the analysis built into the Go toolchain. It detects likely bugs such as mismatched Printf format strings, forgotten cancel calls for a context (lostcancel), and copying values that contain locks (copylocks). go test automatically runs a subset of the go vet checks.
golangci-lint is the de facto standard runner that executes many linters together. It runs fast thanks to parallel execution and caching, and shares its settings through .golangci.yml. staticcheck is integrated into it. In version 2 (v2) the configuration file format differs from v1, and you must write version: "2".
| Area | v2 key | Contents |
|---|---|---|
| Linters | linters | default (such as standard), enable / disable, and per-linter settings in linters.settings |
| Exclusions | linters.exclusions | Presets (such as comments) and exclusion rules by path or text |
| Formatters | formatters | Enable gofmt, goimports, gofumpt, and others with enable |
default: standard enables errcheck, govet, ineffassign, staticcheck, and unused.
When to use go generate
go generate simply finds special comments of the form //go:generate command in source code and runs them. It is not run automatically by go build or go test. Commit the generated output to the repository, and have CI verify that generation was not forgotten.
- Generating
String()methods for enumerated types withstringer - Generating mocks with
mockgenormoq - Generating code from SQL or OpenAPI with
sqlcoroapi-codegen - Generating Protocol Buffers code
By convention, generated files contain a comment of the form // Code generated ... DO NOT EDIT., and linters and editors use it to treat generated code specially.
Integrating into CI
The basic approach is to use the same commands locally and in CI, and to check the following in order.
- Formatting (
gofmt -l, orgolangci-lint fmt --diff) go vet ./...andgolangci-lint run- Generated code and
go.mod/go.sumare up to date (no diff aftergo generate ./...andgo mod tidy) - Tests (
go test -race ./...; see Testing for details) - Vulnerability check (
govulncheck ./...)
CI for build and deployment is covered in Build and Deploy.
2. .NET Mapping
| .NET (C#) | Go |
|---|---|
| dotnet format / EditorConfig | gofmt / goimports |
| Roslyn Analyzers | go vet / staticcheck / golangci-lint |
| Source Generators | go generate + code generation tools |
Additional mappings that complement the table above:
| .NET (C#) | Go |
|---|---|
dotnet format --verify-no-changes | Check that the output of gofmt -l . is empty / golangci-lint fmt --diff |
dotnet_diagnostic.*.severity in .editorconfig | linters / exclusions in .golangci.yml |
TreatWarningsAsErrors | golangci-lint run exits with code 1 when it finds issues |
#pragma warning disable CA1822 | //nolint:linter-name // reason |
| Analyzer packages from NuGet | Linters bundled with golangci-lint (no extra packages) |
dotnet-tools.json (local tools) | The tool directive in go.mod (Go 1.24+) and go tool |
| Source Generator (runs automatically during build) | go generate (run explicitly, and commit the output) |
| T4 templates | A custom generator combining text/template and //go:generate |
NuGet audit / dotnet list package --vulnerable | govulncheck ./... |
3. Common Pitfalls for C# Developers
There are no formatting options
You don't need the .editorconfig habit of tuning indentation width or brace position. gofmt effectively has no settings, so everyone gets the same output. Just enable format-on-save in your editor.
gofmt -l exits with code 0 even when there are differences
To make CI fail on unformatted code, you have to check yourself whether the output of gofmt -l . is empty. There is no dedicated flag like dotnet format --verify-no-changes. Using golangci-lint fmt --diff, or a setup where golangci-lint run includes formatters, makes this easier.
Analyzers don't run during the build
In C#, Roslyn Analyzers run during compilation and show up in the build as warnings or errors. The Go compiler makes unused variables and unused imports errors, but does no other analysis. go vet and golangci-lint are separate commands that you must run yourself.
golangci-lint v1 and v2 configurations are not compatible
Many articles online still show the v1 format (linters-settings, issues.exclude-rules, and so on). v2 requires version: "2" and reorganizes settings into linters.settings, linters.exclusions, and formatters. golangci-lint migrate converts an old configuration, and golangci-lint config verify validates a configuration file.
Don't enable every linter
Enabling a large number of linters produces more opinionated findings and degrades the developer experience. Start from default: standard and add only the ones that are effective at catching bugs. When adopting it in existing code, the new-from-rev setting (issues.new-from-rev), which targets only newly written code, lets you introduce linting gradually.
Add a reason to //nolint
Specify the target linter, as in //nolint:errcheck, and add a reason as a comment. A bare //nolint without a linter name silences every linter, so avoid it. Enabling nolintlint detects unused //nolint directives and ones without an explanation.
go generate doesn't run automatically
Unlike a Source Generator, building does not generate anything. After changing a generation source, run go generate ./... and commit the output together. If you forget, the build can pass with stale generated output. Detect this in CI by checking for a diff after regeneration.
Align tool versions across the team
If everyone runs go install foo@latest, tool versions differ from person to person. From Go 1.24 on, go get -tool records a tool directive in go.mod, and go tool name runs the version pinned in go.mod. For golangci-lint itself, the safe choice is to follow the official documentation, which recommends installing the binary or using the official GitHub Action.
4. Code Examples
Example 1: Everyday commands
# Formatting (show diff, rewrite, with simplification)
gofmt -l .
gofmt -d .
gofmt -s -w .
# Organize imports (install only once)
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .
# Standard static analysis
go vet ./...
# golangci-lint v2
golangci-lint run
golangci-lint fmt # run formatters (rewrites files)
golangci-lint fmt --diff # show the diff only
golangci-lint config verify
# Vulnerability check for dependencies
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
These correspond to dotnet format and the IDE's code cleanup in C#.
Example 2: golangci-lint v2 configuration
An example .golangci.yml. It builds on standard and adds linters that are effective at catching bugs.
version: "2"
run:
timeout: 5m
linters:
default: standard # errcheck, govet, ineffassign, staticcheck, unused
enable:
- bodyclose # forgotten http.Response.Body close
- errorlint # places that should use errors.Is / errors.As
- gocritic
- misspell
- nolintlint # detect improper //nolint
- revive
- unparam
settings:
errcheck:
check-type-assertions: true
govet:
enable:
- shadow # variable shadowing
nolintlint:
require-specific: true # require the //nolint:linter-name form
require-explanation: true
exclusions:
presets:
- comments
- std-error-handling
rules:
- path: _test\.go
linters:
- errcheck
- unparam
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- example.com/app
default accepts standard, all, none, and fast. Check the official documentation for the golangci-lint version you use for the individual settings.
Example 3: Writing //nolint
// Good: restrict the linter name and add a reason
defer f.Close() //nolint:errcheck // read-only file, so the Close error can be ignored
// Bad: disables every linter
defer f.Close() //nolint
This corresponds to #pragma warning disable CA2000 and [SuppressMessage] in C#.
Example 4: go generate and the tool directive
An example that generates String() for an enumerated type with stringer.
package color
//go:generate go tool stringer -type=Color
type Color int
const (
Red Color = iota
Green
Blue
)
Pin stringer in go.mod (Go 1.24+).
go get -tool golang.org/x/tools/cmd/stringer
A line like the following is added to go.mod.
tool golang.org/x/tools/cmd/stringer
go generate ./... # generates color_string.go
go tool stringer -help
go install tool # install every tool in go.mod into GOBIN
A go generate directive runs in the directory containing that file. Multiple directives run in order from the top. Up to Go 1.23, the common way to pin tools was a blank import in tools.go (_ "golang.org/x/tools/cmd/stringer" with //go:build tools).
Example 5: Lint and verification in GitHub Actions
name: lint
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check formatting
run: test -z "$(gofmt -l .)"
- name: Vet
run: go vet ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.5
- name: Verify generated code is up to date
run: |
go generate ./...
git diff --exit-code
- name: Verify go.mod is tidy
run: go mod tidy -diff
- name: Test
run: go test -race -cover ./...
- name: Vulnerability check
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
Pin the version of golangci-lint-action to the v2 release you use. go mod tidy -diff is available from Go 1.23, and it prints a diff and fails if changes are needed.
Example 6: The equivalent setup in C#
On the C# side, a quality gate is commonly configured with Directory.Build.props and .editorconfig like this.
<Project>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</Project>
# .editorconfig
[*.cs]
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1062.severity = warning
In Go, a single .golangci.yml file and a sequence of CI commands play this role. The difference is that analysis is not part of the build itself, so you run it explicitly in CI.
5. Summary
- Leave formatting to
gofmt(orgoimports/gofumpt) and don't debate settings. In CI, check whether the output ofgofmt -lis empty. go vetis the standard static analysis, andgolangci-lintruns many linters, includingstaticcheck, together. In v2 you configure it withversion: "2",linters, andformatters.- Start linting from
default: standardand add only high-value linters. Always write the linter name and a reason in//nolint. go generateis a command you run explicitly, and the output is committed. In CI, regenerate and confirm there is no diff.- From Go 1.24 on, the
tooldirective ingo.modandgo toollet you pin development tool versions. - In CI, check formatting,
go vet,golangci-lint, consistency of generated code andgo.mod,go test -race, andgovulncheck, in that order.