Linting and Formatting: gofmt Is Non-Negotiable

In C#, code formatting is a matter of preference. Tabs or spaces? Braces on the same line or next? Teams debate, .editorconfig files proliferate, and nobody quite agrees.

In Go, formatting is not a discussion. gofmt defines the One True Format, and everyone uses it. There is no debate because there’s no choice.

This sounds authoritarian. It’s actually liberating.

gofmt: The Formatter

Every Go file should be formatted by gofmt:

gofmt -w .              # format all files in place
go fmt ./...            # same thing, via go command

gofmt handles:

  • Indentation (tabs, not spaces)
  • Brace placement (same line, always)
  • Spacing around operators
  • Import grouping
  • Line length (it doesn’t wrap, but long lines are a code smell)

There are no options. No configuration. One format for all Go code, everywhere.

The Philosophy

Rob Pike (Go co-creator): “Gofmt’s style is no one’s favourite, yet gofmt is everyone’s favourite.”

The point isn’t that gofmt’s choices are optimal. The point is that having a choice wastes time. Every Go codebase looks the same, which means:

  • No bikeshedding about style
  • Pull reviews focus on logic, not formatting
  • New team members read unfamiliar code easily
  • Tooling (IDEs, linters) can assume standard formatting

goimports: gofmt Plus Import Management

goimports does everything gofmt does, plus:

  • Adds missing imports
  • Removes unused imports
  • Groups imports (stdlib, external, internal)
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .

Most developers use goimports instead of plain gofmt. Configure your editor to run it on save.

Linting with golangci-lint

Formatting ensures consistency. Linting catches bugs and enforces best practices.

golangci-lint is the standard meta-linter—it runs many linters in one tool:

# Install
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Run
golangci-lint run

Default linters catch:

  • Unused variables and imports
  • Ineffective assignments
  • Missing error checks
  • Suspicious constructs

Common Linters

golangci-lint bundles dozens of linters. Key ones:

LinterPurpose
errcheckUnchecked errors
gosimpleSimplification suggestions
govetSuspicious constructs
ineffassignIneffectual assignments
staticcheckAdvanced static analysis
unusedUnused code
gosecSecurity issues
misspellSpelling mistakes
gocycloCyclomatic complexity
gocriticOpinionated style issues

Configuration

Create .golangci.yml:

linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - gosec
    - misspell
    
linters-settings:
  errcheck:
    check-type-assertions: true
  govet:
    check-shadowing: true
  gocyclo:
    min-complexity: 15

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - gosec  # less strict in tests

Run with:

golangci-lint run
golangci-lint run --fix  # auto-fix where possible

staticcheck

The most powerful individual linter. It catches subtle bugs:

// staticcheck catches this
if x == nil {
    return x.String()  // nil dereference
}

// And this
for i := range items {
    go func() {
        process(items[i])  // captures loop variable
    }()
}

Many issues that would be runtime errors become compile-time warnings.

Comparing to .NET Analyzers

Feature.NET AnalyzersGo Linting
Format enforcementOptional (EditorConfig)Mandatory (gofmt)
Built-inSome analyzersgofmt, go vet
Meta-linterStyleCop, Roslyngolangci-lint
IDE integrationExcellentGood
CI integrationEasyEasy
Auto-fixMany rulesSome rules

.NET has the advantage of the Roslyn compiler platform—analyzers can be very sophisticated. Go’s linters are simpler but catch most issues.

Editor Integration

VS Code

Install the Go extension. Add to settings.json:

{
    "go.formatTool": "goimports",
    "go.lintTool": "golangci-lint",
    "editor.formatOnSave": true,
    "[go]": {
        "editor.defaultFormatter": "golang.go",
        "editor.codeActionsOnSave": {
            "source.organizeImports": true
        }
    }
}

GoLand

Built-in support. Enable “Reformat code” and “Optimize imports” on save.

Vim/Neovim

Use vim-go or configure gopls (the Go language server) with your LSP setup.

CI Integration

Add to your pipeline:

# GitHub Actions
- name: golangci-lint
  uses: golangci/golangci-lint-action@v4
  with:
    version: latest

Or manually:

# Install
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin

# Run
golangci-lint run --timeout 5m

Pre-commit Hooks

Enforce formatting before commit:

# .git/hooks/pre-commit
#!/bin/sh
gofmt -l . | read && echo "Files not formatted" && exit 1
golangci-lint run && exit 0 || exit 1

Or use pre-commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/golangci/golangci-lint
    rev: v1.55.0
    hooks:
      - id: golangci-lint

The Honest Take

Go’s enforced formatting is one of its best features. No arguments, no decisions, no config files.

What Go does better:

  • No bikeshedding—ever
  • All code looks the same
  • gofmt is fast and universal
  • No configuration to maintain

What .NET does better:

  • More formatting options (if you want them)
  • Roslyn analyzers are very powerful
  • Better code action suggestions
  • More sophisticated refactoring

The verdict: You’ll miss zero formatting decisions. You won’t miss the arguments about formatting. golangci-lint catches most issues; enable it and move on.

The Go community’s attitude is refreshing: format your code, lint it, and spend your energy on actual problems.


Next up: code generation with go generate—why Go developers love generating code.