Introduction — The Formatter Debate, Revisited
An article about gofumpt recently became a hot topic again on GeekNews. Over this formatter, which applies stricter rules on the grounds that "gofmt alone is not enough," the debate reignited over just how far opinionated tooling should be allowed to enforce.
What is interesting is that this debate is not a mere matter of taste. Seemingly trivial decisions, like whether to indent with tabs or spaces or where to place braces, have in fact consumed a surprising amount of teams' time and emotion. The Go camp settled the matter with a single tool, gofmt, and that decision remains a model that other language ecosystems still envy.
In this article, we start from the philosophy of automatic formatting, compare gofmt with the stricter gofumpt, and set them against tools from other languages like prettier and black. We then take a deep look at how opinionated tooling weaves into team consensus and CI, and what its pros and cons are.
The Philosophy of Automatic Formatting — A Tool to End the Debate
Code style debates are as old as the history of software. And most of those debates were matters of taste with no right answer. Either way, code works fine as long as it is consistent. Yet people argued endlessly over these tiny differences.
The Go designers' insight lay here: "There is no right answer for style, so let us eliminate the debate itself." gofmt is not a tool to enforce the correct style; it is a tool to end debate about style. Which style it is does not matter. What matters is the fact that everyone uses the same style.
This philosophy is summed up in one sentence: "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
// No matter how the author writes it
func add(a int,b int)int{return a+b}
// After passing through gofmt, it always looks the same
func add(a int, b int) int {
return a + b
}
The key is "removing choices." With no choices, there is nothing to argue about. And with no arguing, that energy can be poured into solving real problems.
The Birth of gofmt — A Decision in Go's Early Days
Behind the fact that this philosophy was implemented as a tool, not just a slogan, lies a decision made in Go's earliest days. From its public release in 2009, Go shipped with gofmt. Nailing a formatter down as a standard tool at the very moment a language enters the world was a bold choice for the time. Most languages only created a formatter belatedly, after several style guides had proliferated over the years.
The reason the Go team standardized a formatter so early was clear: you have to nail it down before style fragmentation sets in. Once several styles take root in a codebase, introducing a standard afterward becomes politically almost impossible, because everyone has grown accustomed to their own style. Go blocked this problem at its source by offering a single answer before the chaos could even begin.
The mindset of the Go designers, including Rob Pike and Russ Cox, is captured well in the so-called "Go Proverbs." These proverbs are sentences that distill the values of simplicity and clarity that Go pursues.
- "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
- "A little copying is better than a little dependency."
- "Clear is better than clever."
- "The bigger the interface, the weaker the abstraction."
The spirit running through these proverbs is consistent: choose clarity over cleverness, simplicity over a show of abstraction. gofmt is the result of applying exactly this spirit to the surface of code. Instead of asking "which style is cleverer," it chooses the clarity of "everyone sees the same shape."
Interestingly, gofmt went beyond being a mere formatter and influenced the whole tooling culture of Go. The way gofmt handles the AST (abstract syntax tree) of code became the foundation on which later tools like `gorename` and `gopls` safely transform code. In other words, gofmt was also the starting point of the Go tooling ecosystem's principle of "treating code as structure, not text."
gofmt — The Formatter That Became the Standard
gofmt has been included in the standard Go toolchain from the very beginning. No separate installation, no configuration file required. This "no configuration" trait is by design. If configuration were possible, people would argue over the configuration too.
Format a single file
gofmt -w main.go
Format the entire package
go fmt ./...
Preview changes only (print diff without writing)
gofmt -d main.go
What gofmt handles is mainly the following:
- Unifying indentation to tabs
- Normalizing whitespace around operators and after commas
- Sorting and grouping import blocks
- Aligning struct fields and alignable code
- Removing unnecessary parentheses
One thing must be made clear here. gofmt deliberately touches only the "minimum." Some areas where opinions might differ from person to person, without affecting behavior, are still left untouched. It is precisely that gap that gofumpt moved into.
gofumpt — One Step Beyond gofmt
gofumpt started from the conviction that "gofmt is right, but not strict enough." The name itself is a kind of pun on gofmt: all of gofmt's output remains valid as-is, while additional rules are applied on top. That is, code that passes gofumpt always passes gofmt too (a superset).
Here are a few examples of the rules gofumpt additionally enforces.
// Cases gofmt allows but gofumpt tidies up
// (1) Remove unnecessary blank lines at the start and end of a function body
func before() {
doSomething()
}
func after() {
doSomething()
}
// (2) Encourage grouping of short variable declarations
// (3) Collapse unnecessarily long blank lines
// (4) Enforce a consistent form for some composite literals
gofumpt's appeal lies in being "cleaner with no extra configuration." It inherits gofmt's zero-configuration philosophy while automatically doing the additional tidying that people commonly agree on. This is why many teams adopt gofumpt instead of gofmt as their default.
That said, remember that gofumpt is not the official standard. gofmt is the single standard endorsed by the Go team, while gofumpt is a third-party tool that layers one more level of opinion on top. This subtle difference is the starting point of the politics we will look at next.
A Deeper Look at gofumpt's Rules
So far we have looked at gofumpt's rules only in broad strokes. Now let us examine some more concrete rules in code. It is the accumulation of such fine-grained rules that creates gofumpt's impression of being "cleaner with no extra configuration."
First, it unifies octal literal notation into a modern form. It prefers the `0o` prefix introduced in Go 1.13.
// gofmt allows both, but gofumpt prefers the 0o form
perm := 0755 // old octal notation
perm := 0o755 // the explicit notation gofumpt favors
Second, it nudges you to group consecutive parameters of the same type in function signatures.
// verbose form
func move(x int, y int, z int) {}
// the grouped form gofumpt recommends
func move(x, y, z int) {}
Third, it handles import group sorting more strictly. gofmt sorts imports too, but gofumpt tidies up meaningless empty groups scattered between the standard library and third-party packages.
// gofumpt tidies up unnecessarily split import groups
"fmt"
"strings"
"github.com/foo/bar"
)
Fourth, its rules for blank lines are tighter. It cleans up blank lines stuck at the start and end of a block, two or more consecutive blank lines, and so on.
// blank-line patterns gofumpt tidies up
type Config struct {
Name string
Port int
}
// a single blank line inserted meaninglessly between fields, as above,
// also becomes a candidate for tidying depending on context
Seen one by one, these rules are trivial. But applied consistently across an entire codebase, they produce code so homogeneous you cannot tell who wrote it. This very homogeneity is the effect an opinionated tool aims for.
goimports — Combining Formatting with Import Management
A tool you cannot leave out when discussing the gofmt family is goimports. goimports performs all the formatting gofmt does, while additionally managing import statements automatically. It removes unused imports and automatically finds and adds packages that are used in the code but not imported.
install
go install golang.org/x/tools/cmd/goimports@latest
use it like gofmt, but it also tidies imports
goimports -w main.go
option to separate local package groups
goimports -local github.com/myorg -w ./...
The `-local` option is especially useful. It sorts the standard library, third-party packages, and your organization's internal packages into separate groups, so that just by looking at the import block you can see at a glance where each dependency comes from.
Many developers use goimports rather than gofmt as their default formatter in daily work. Import tidying is always necessary anyway, and goimports handles it together with formatting in one pass. gofumpt can also be combined with goimports, so the combination of "gofumpt-level strictness plus automatic import management" is very popular in practice.
Editor Integration — The Experience of Auto-Tidying on Save
The moment you feel a formatter's value most in daily work is when "format on save" is enabled. Without the developer thinking about it, the code is tidied into the standard form the instant the file is saved. This way, formatting is no longer "a task you must remember to run" but "something that just happens."
In the Go camp, gopls (the official Go language server) is at the center of this experience. gopls integrates with editors and handles not only features like code completion and go-to-definition but also formatting and import tidying on save. In most editors, you can hook gofmt or gofumpt to the save trigger through gopls.
// VS Code settings.json example — format with gofumpt on save
{
"editor.formatOnSave": true,
"gopls": {
"formatting.gofumpt": true
}
}
The important point here is that the effect is maximized only when the whole team shares the same editor settings. If one person turns on format-on-save and another turns it off, formatting becomes uneven from commit to commit and diffs grow messy. That is why many teams commit the editor settings themselves into the repository, or share a minimal set of common rules using a tool like EditorConfig, which we will see shortly.
Comparison with Other Languages — prettier, black, rustfmt
The model Go built with gofmt had a major influence on other language ecosystems. Let us compare a few.
| Tool | Language | Configurability | Philosophy |
| --------- | ----------- | ------------------- | -------------------------------- |
| gofmt | Go | Almost none | Enforce a standard, end debate |
| gofumpt | Go | None (stricter) | gofmt + extra tidying |
| prettier | JS/TS etc. | Some | Opinionated with some leeway |
| black | Python | Almost none | "The uncompromising formatter" |
| rustfmt | Rust | Quite a lot | Recommend a standard, allow config |
Here an interesting spectrum emerges. At one end are gofmt and black, which allow almost no configuration; at the other is rustfmt, which allows considerable configuration. prettier sits somewhere in between.
Python's black calls itself "The Uncompromising Code Formatter." The attitude of an opinionated tool is right there in the name. black, too, minimizes configurability to shut down debates like "how many characters per line."
prettier, by contrast, leaves some options (quote style, semicolons, etc.) to give a little leeway. This is a pragmatic compromise reflecting the diversity of the JavaScript ecosystem, but it also produces the side effect that "debates break out over those options too." The moment you allow configuration, the configuration itself becomes a new subject of debate.
The Configurability Spectrum — dprint, Biome, clang-format, EditorConfig
The table above covered only a few representative tools. Widen the view, and formatters form one long spectrum along the axis of "how much configuration do they allow." Understanding the two extremes makes it easier to judge where your team should stand.
At one end are gofmt, gofumpt, and black, which allow almost no configuration. For these, "removing choices" is the goal itself. At the other end are tools that let you configure almost everything. Various compromises sit in between.
- **dprint**: A fast multi-language formatter written in Rust that handles several languages through a plugin architecture in a single tool. It deliberately leaves configurability somewhat more open than prettier.
- **Biome**: An attempt in the JavaScript/TypeScript ecosystem to bundle prettier and ESLint into one. It integrates formatting and linting into a single tool and touts its speed as a strength.
- **clang-format**: The flagship formatter of the C/C++ camp, near the extreme of configurability. It lets you finely tune dozens of options — indentation, brace placement, alignment style — and also provides predefined styles like LLVM, Google, and Mozilla. It is flexible, but precisely because of that flexibility, debates tend to break out over "our team's clang-format config."
- **EditorConfig**: Strictly speaking not a formatter but a configuration standard for sharing a minimal set of common rules (indentation style, line endings, final newline, etc.) across editors. Because it works regardless of language and editor, it is widely used as a "minimal consensus" at the stage before adopting a full-fledged formatter.
.editorconfig example — minimal common rules across editors
root = true
[*]
indent_style = tab
end_of_line = lf
insert_final_newline = true
charset = utf-8
The lesson of this spectrum is clear. The higher the configurability, the more situations a tool adapts to, but the more it takes on a new debate over "how should we configure it." Conversely, the lower the configurability, the less adaptable it is, but the debate disappears. The fact that Go deliberately chose the latter shows that tool design is itself a value judgment.
Linting and Formatting Are Different
Let us clear up a frequently confused distinction before moving on. Formatting and linting have different purposes.
- **Formatting**: Deals with the "shape" of code. Indentation, whitespace, line breaks, and other surface forms that do not affect behavior.
- **Linting**: Points out potential problems with the "content" of code. Unused variables, suspicious comparisons, possible bug patterns, and so on.
Formatting - automatically fixes the shape
gofumpt -w ./...
Linting - points out problems (golangci-lint example)
golangci-lint run ./...
Confusing the two tangles your tool choices. A formatter takes the stance "code that does not look like this gets fixed automatically," while a linter takes the stance "this might be a problem, so let a human judge." Good teams use both but clearly separate their roles. Formatting is left entirely to the machine; linting is taken as warnings for humans to review.
What a Formatter Does Not Touch — Limits and Exceptions
Even an opinionated tool does not decide everything for you. We mentioned earlier that gofmt deliberately leaves some areas untouched, and a prime example is line length. prettier and black automatically wrap long lines with a rule like "at most N characters per line," but gofmt enforces no rule at all about line length. Where to break a line is left to human judgment.
This is by design. Where to break a line is often tied to the semantic structure of the code, so a machine cutting it uniformly can actually hurt readability. For instance, in code like the following, it is better for the author to decide where to break based on meaning.
// a form where a human split the line based on meaning
result := computeScore(
player,
difficulty,
bonusMultiplier,
)
There is another case: sometimes you want to deliberately go against the formatter's decision. As with data aligned into a table or ASCII-art comments, automatic alignment can break things instead. Many formatters provide a "do not touch here" directive for this.
// gofmt aligns comments on adjacent lines, but
// sometimes you want to keep alignment intentionally, as below
var weekdays = []string{
"Mon", // Monday
"Tue", // Tuesday
"Wed", // Wednesday
}
What is interesting is the consistency of gofmt's philosophy. prettier provides an explicit escape hatch like `// prettier-ignore`, but gofmt offers no such general "ignore formatting" directive. The moment you open an escape hatch, a new debate arises over "how far do we ignore?" Instead, gofmt leaves only ways to express intent through the code structure itself, such as inserting a blank line to break alignment. Even in this small difference, the philosophy of "minimizing choices" is consistently on display.
In the end, understanding a formatter's limits is part of using it well. A formatter is not omnipotent, and areas where meaning is involved still remain a human's job. A good tool, by making clear what it does not do, tells you where you should focus.
Team Consensus and CI Enforcement — Turning a Tool Into a Rule
The real value of an opinionated tool shows when it is enforced in CI. No matter how good a formatter is, if someone does not use it, the consistency of the codebase breaks. So many teams nail formatting down as a CI check.
A typical pattern for checking formatting violations in CI
(if any files need changes, print the list and fail)
gofumpt -l . | tee /tmp/fmt.txt
test ! -s /tmp/fmt.txt
In a real CI pipeline, you embed this check as a step in the workflow. In GitHub Actions, for example, it takes the following form.
name: lint
on: [push, pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install gofumpt
run: go install mvdan.cc/gofumpt@latest
- name: Check formatting
run: test -z "$(gofumpt -l .)"
The last step is the key. `gofumpt -l .` prints the list of files whose formatting is off, and if the output is not empty, the check fails. In other words, the rule "if even a single unformatted file exists, block the merge" is nailed down in code.
This one small check changes the landscape of code review. There is no longer any need for remarks like "you are missing a space here" in reviews, because the machine filters those out. Reviewers can focus on what really matters: design and logic.
In 2026, one more trend has been added. As AI coding agents became widespread, a pipeline that automatically tidies agent-generated code with a formatter and linter became standard. Even AI-generated code, after passing through gofumpt, becomes indistinguishable from human-written code. An opinionated tool thus also functions as a leveling device that converges the output of humans and machines into the same form.
Pros and Cons of Opinionated Tools
Now let us lay out the light and shadow of opinionated tools.
Pros
- **Ending debate**: The energy spent on style can be redirected to real problems.
- **Consistency**: The entire codebase has the same shape and is easy to read.
- **Simpler onboarding**: New members do not need to memorize a style guide; the tool aligns things for them.
- **Focused reviews**: Surface-level remarks disappear, letting reviewers focus on substance.
Cons and Criticisms
- **Loss of flexibility**: Even when a more readable form exists in a specific situation, you must follow the form the tool enforces.
- **The authority-of-standard problem**: Adopting an unofficial tool like gofumpt can create another debate: "Why use this instead of the official gofmt?"
- **Injection of opinion**: The taste of the tool's author is enforced on everyone. This is why it is called "the politics of code style": someone's opinion gains power in the form of a tool.
There is one important insight here. The reason an opinionated tool is good is not because its opinion is "correct," but because the opinion is "fixed into one." That is, the tool's value lies not in the legitimacy of its content but in the finality of its decision. Once you understand this, you see how much arguing over "whether this rule is right or wrong" misses the point.
A Real-World Case — Introducing a Formatter to a Large Codebase for the First Time
The theory is clean, but reality is different. Suppose that one day you decide to introduce gofumpt to a huge codebase that has grown for years without any formatter. The biggest worry is "how do we tidy up the hundreds of thousands of lines accumulated over that time, all at once?"
The simplest approach is to format everything in one go and make it a single giant commit.
a giant commit that formats the whole repository at once
gofumpt -w ./...
git add -A
git commit -m "style: apply gofumpt across the entire codebase"
This approach looks clean but has one serious side effect: `git blame` pollution. Because this giant format commit touches almost every line of almost every file, running `git blame` afterward shows the "last changer" of countless lines as this format commit rather than the actual author. Tracking who wrote this code and why becomes very difficult.
Fortunately, Git has a solution for this problem: a feature to designate certain commits to be ignored in blame calculations. You create a `.git-blame-ignore-revs` file at the repository root and write down the hashes of the format commits to ignore.
add the format commit's hash to the ignore list
echo "a1b2c3d4e5f6 # style: apply gofumpt across the entire codebase" >> .git-blame-ignore-revs
make Git always reference this file
git config blame.ignoreRevsFile .git-blame-ignore-revs
With this setup, `git blame` skips the format commit and shows the actual author from before it. Hosting services like GitHub and GitLab also recognize this file automatically and reflect it in the blame view of their web UI. In other words, the biggest drawback of the "giant format commit" disappears.
To summarize the migration strategy:
1. **All at once, in a dedicated commit**: Never mix formatting changes with logic changes. Make a single commit that changes only formatting.
2. **Register it in the ignore list**: Add that commit hash to `.git-blame-ignore-revs` to prevent blame pollution.
3. **Clean up in-flight branches**: A giant format commit causes conflicts in every branch that is in progress. Announce it to the team in advance and, if possible, pick a time when there is little ongoing work.
4. **Start CI enforcement immediately**: Turn on the CI check right after tidying up once, to prevent things from getting messy again.
What this case shows is that introducing a formatter is not a single command line but a small project that must consider the team's collaboration flow and tooling together. No matter how elegant a tool's philosophy is, the reality of adoption hinges on details like these.
A Practical Adoption Guide
Finally, here are recommendations for adopting opinionated tools in practice.
1. **Pick one and stick with it**: Whether gofmt or gofumpt, once the team has picked one, stop debating. Remember that the tool's very purpose is to end debate.
2. **Enforce it in CI**: Left to individual discretion, consistency will inevitably collapse. Automate the check.
3. **Format on save in the editor**: Developers will always produce formatted code without even thinking about it.
4. **Separate it from linting**: Formatting is auto-fix, linting is human review; divide the roles.
5. **Run AI-generated code through the same pipeline**: Whatever the source, the codebase should have one shape.
Conclusion
The history of code formatting tools is a story about how to elegantly conclude a seemingly trivial problem. Go presented a powerful model with gofmt: "remove choices to end debate," and gofumpt stepped one further on top of it. black, prettier, and rustfmt each varied this philosophy in their own way.
What opinionated tools teach us is that not every decision needs to go through debate. Some decisions just need someone to make them. Even if the decision is not perfect, the very fact that everyone follows it creates greater value than perfection. This insight, which began in the small domain of code style, is becoming even more important in an era when AI generates code en masse.
Seen from a little further away, the question gofmt poses reaches far beyond code style. "How far do we settle by consensus, and from where do we hand things over to a tool?" is in fact the fundamental question of all collaboration. Opinionated tooling offers one clear answer to it: in areas where there is no right answer, the act of making a decision matters more than the content of the decision. A great many of the countless trivial choices we face every day — file structure, naming conventions, commit message format — fall under this lesson. gofmt simply showed the answer first in that small domain; its spirit reaches much further. A good tool does not set us free. Rather, in exchange for taking away small freedoms, it gives back a larger one. Deciding what we no longer have to think about — that is the most precious gift an opinionated tool offers.
References
- [gofumpt — GitHub repository](https://github.com/mvdan/gofumpt)
- [Go official — gofmt documentation](https://pkg.go.dev/cmd/gofmt)
- [The Go Blog — go fmt your code](https://go.dev/blog/gofmt)
- [Go Proverbs (Gofmt's style is no one's favorite...)](https://go-proverbs.github.io/)
- [goimports — golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
- [EditorConfig](https://editorconfig.org/)
- [The Go Programming Language Specification](https://go.dev/ref/spec)
- [Black — The Uncompromising Code Formatter](https://black.readthedocs.io/en/stable/)
- [Prettier — Opinionated Code Formatter](https://prettier.io/)
- [rustfmt — GitHub repository](https://github.com/rust-lang/rustfmt)
- [golangci-lint official documentation](https://golangci-lint.run/)
- [GeekNews](https://news.hada.io/)
현재 단락 (1/204)
An article about gofumpt recently became a hot topic again on GeekNews. Over this formatter, which a...