Gofmt and Why the Whole Go Community Agrees on Style
gofmt rewrites Go source into one canonical layout, so style arguments never happen. See a diff, the useful flags, the -s simplifications and a CI check.

Most languages argue about style for years and then settle it with a style guide and a linter configuration that everyone resents. Go settled it by shipping the formatter with the language in 2009 and making it close to impossible to configure. Tabs or spaces, where braces go, how to align a struct, all decided once, applied automatically. The visible result is that a Go file from any project looks like a Go file from any other, and a diff contains only the change someone meant to make. This article shows what gofmt changes on a deliberately messy file, which flags matter, what it does not do and how to run it so nobody ever thinks about it again. It belongs in the Go basics path, it is the standing companion of doc comments, and the rules it cannot enforce, such as names, are in naming conventions.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
What it does to a messy file
Here is a file that compiles and offends every convention: unindented blocks, inconsistent spacing, a redundant composite literal, a range with a blank index and a slice expression that spells out its own length. The command is gofmt -d -s ., which prints what it would change as a diff:
package main
import (
"fmt"
"os"
)
type Config struct {
Name string
Port int
Tags []string
}
func main() {
c := []Config{Config{Name:"a", Port:1}, Config{Name:"b"}}
for i, _ := range c {
if len(c[i].Tags)==0 { fmt.Println(c[i].Name) }
}
s := []int{1,2,3}
fmt.Println(s[1:len(s)])
var x int = 5
_ = x
_ = os.Args
}diff main.go.orig main.go
--- main.go.orig
+++ main.go
@@ -1,23 +1,25 @@
package main
import (
-"fmt"
-"os"
+ "fmt"
+ "os"
)
type Config struct {
-Name string
-Port int
-Tags []string
+ Name string
+ Port int
+ Tags []string
}
func main() {
- c := []Config{Config{Name:"a", Port:1}, Config{Name:"b"}}
- for i, _ := range c {
- if len(c[i].Tags)==0 { fmt.Println(c[i].Name) }
+ c := []Config{{Name: "a", Port: 1}, {Name: "b"}}
+ for i := range c {
+ if len(c[i].Tags) == 0 {
+ fmt.Println(c[i].Name)
+ }
}
- s := []int{1,2,3}
- fmt.Println(s[1:len(s)])
+ s := []int{1, 2, 3}
+ fmt.Println(s[1:])
var x int = 5
_ = x
_ = os.ArgsRead the diff from the top. The imports and struct fields were indented with tabs, and the field names and types were aligned in columns, so Port int lost its stray spaces. Spacing around operators was normalized (== and : got their spaces), and the one-line if body was expanded onto its own lines, because gofmt never keeps a block on a single line. Three differences were not layout at all: the repeated Config type name dropped from the literal, for i, _ := range losing its blank, and s[1:len(s)] becoming s[1:]. Those came from the -s flag.
Two things the diff does not touch. The var x int = 5 stays: gofmt -s does not remove a redundant type, and a linter such as revive reports it. And the tool never changes behavior. Everything it does is a layout change, with the -s simplifications limited to rewrites that the spec guarantees are equivalent.
The flags that matter
| Command | What it does |
|---|---|
gofmt -l . | Lists files whose formatting differs, and prints nothing for a clean tree |
gofmt -d . | Prints the diff for each differing file |
gofmt -w . | Rewrites the files in place |
gofmt -s -w . | Also applies simplifications: []T{T{}} to []T{{}}, for i, _ := range to for i := range, s[a:len(s)] to s[a:] |
gofmt -r 'a[b:len(a)] -> a[b:]' -w . | Applies a rewrite rule across the files |
go fmt ./... | Runs gofmt -l -w on every package |
The -r flag deserves a note. A rule like -r 'oldFunc(x) -> newFunc(x)' matches expressions by syntax tree, with single lowercase letters standing for any expression. It makes a one-line API migration across a repository safe, since it understands the grammar, unlike a regular expression. Anything more complex than a call renaming belongs to a proper refactoring tool, and gopls provides most of those through the editor.
It does not fix everything
gofmt knows nothing about meaning. It does not sort imports across groups, remove unused ones, wrap long lines or rename anything. Its scope is the shape of the source, and the missing pieces have separate tools:
- Imports are handled by
goimports, which does everythinggofmtdoes and also adds missing imports and removes unused ones. Most editors run it on save instead of plaingofmt. - Line length has no limit in
gofmt. It leaves a long line alone, and the community convention is to break lines at natural points yourself. Some teams addgolinesfor it. - Unused variables and imports are compiler errors.
- Names and design are a linter's job, with
staticcheckorgolangci-lint.
Doc comments are the one area where it reaches into prose: since Go 1.19 it reformats them into the canonical layout, as the doc comments article describes, so lists and code blocks inside comments are re-indented without your help.
Running it so that no one thinks about it
There are three places to run it, and a project should use all of them.
In the editor, on save. The Go extension for VS Code, GoLand, and the Neovim and Emacs plugins all call gopls or goimports when you save. After that, you never type a space of indentation by hand, and the formatting of your code is a thing that happens to it.
In a pre-commit hook or a make target. go fmt ./... before a commit guarantees the diff is clean.
In CI, as a check that fails. Format on a developer's machine is a courtesy, and a CI check is the guarantee. The command prints the offending files and the exit status makes the job fail:
test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }An empty tree prints nothing and passes. If a file differs, the script lists it and fails. Run it from the repository root, and exclude generated or vendored directories if you keep any, since those need not follow the rule.
Why one style is worth the loss of your preference
The Go proverb says it better than I can: "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite." Nobody loves every choice the tool makes, and nobody has to argue about them either. The benefits are concrete. Code review sees only semantic changes, since whitespace diffs vanish. A new contributor needs no style guide, because their editor produces the right layout. Tools can rewrite code and print it back without producing a noisy diff, which is how go fix and refactoring tools work at all. And reading unfamiliar code is faster when the shape is always the same, because your eye learns where things are.
Tabs for indentation was the choice that annoys newcomers most. The reason is practical: the tool aligns with spaces and indents with tabs, so each reader can set the tab width in their editor without touching the file. If your editor shows eight-column tabs and you prefer four, change the editor, and the source stays the same.
When gofmt output surprises you
Three behaviors are worth knowing. Comment alignment can shift when you edit neighboring lines, since trailing comments in a block are aligned together, and it is the reason a diff sometimes touches lines you did not edit. Blank lines are preserved but collapsed to at most one, so you control paragraphing in a function body. And generated files with a // Code generated ... DO NOT EDIT. header should be formatted by their generator, not hand edited. If a formatting rule truly gets in the way, for example a hand-aligned table in a test, the escape hatch is to move the data to a variable that formats naturally or to a file in a data format. Do not fight the tool, because the tool wins every vote on a shared codebase. The gofmt command documentation lists every flag, and the Go blog's article go fmt your code explains the original motivation.


