JavaRush /Courses /Go SELF /Quality pass: fmt/test/vet/lint

Quality pass: fmt/test/vet/lint

Go SELF
Level 38, Lesson 0
Available

1. Quality pass

If you look at a project through a beginner’s eyes, it’s easy to think: “The code compiles — so it works.” That’s cute, but it’s roughly like calling a car “fine” because the door closes. In Go, the culture is different: historically, the language was designed so that tools are part of day-to-day development, not something exotic for “senior engineers in an ivory tower.” That’s why, alongside the compiler and tests, the ecosystem has always had gofmt and the go command, which make development predictable and automatable.

By a “quality pass” (quality gate) we’ll mean the minimal set of steps after which you can honestly say: “OK, at least this change isn’t embarrassing to show the team.” Today we’ll lock in this exact order:

format (gofmt/goimports) → tests (go test) → static analysis (go vet) → lint

And an important idea: this is not a “magic sequence,” it’s a way to quickly get useful signal and avoid drowning in noise.

2. Why the fmt → test → vet → lint order matters more than it seems

There’s a common pain point: you run a linter, it spits out 60 findings, you start fixing style, and then suddenly you realize tests don’t pass at all, and somewhere fmt.Printf prints a string using %d. That’s the case where you’re painting the walls while the kitchen is on fire.

The order is needed for the “remove noise first, then catch bugs” strategy. Formatting and imports stabilize the code so Git diffs are meaningful and so the next tools don’t complain about trivia. Then tests validate behavior. After that, vet catches what the compiler usually lets through (often these really are potential defects). And only then do linters bring the project in line with maintainability agreements.

An easy way to remember: gofmt makes code readable, go test makes it verifiable, go vet makes it less sneaky, and linters make it less likely to “drift apart” over time.

Let’s draw it as a mini diagram:

flowchart LR
  A[fmt: gofmt/goimports] --> B[test: go test ./...]
  B --> C[vet: go vet ./...]
  C --> D[lint: staticcheck/golangci-lint]
  D --> E[ready for review]

3. Step 1: formatting (gofmt, goimports)

Formatting in Go is not “personal preference,” it’s an ecosystem-level agreement. In the “spaces vs tabs” debate, gofmt wins — and honestly, it’s a rare case where life gets easier for everyone. Go’s tools are historically strong precisely because the language and the standard library support the tooling approach well.

In our learning app (let’s call it tasker) there’s a task package with task logic, and cmd/tasker is the CLI entry point. Imagine you hacked some code together on emotion and it looks like this:

package task
import "strings"
func NormalizeTitle(s string) string {return strings.TrimSpace(s)}

Does it compile? Yes. Is it pleasant to read? Well… only if you like code that looks like a message from a cat that walked across the keyboard.

A normal formatting pass usually looks like this (run from the project root):

gofmt -w .
goimports -w .

The reason to split them is simple: gofmt brings code to the standard shape, and goimports additionally fixes imports (removes unused ones, adds missing ones, sorts them). After that your code becomes this:

package task

import "strings"

func NormalizeTitle(s string) string {
	return strings.TrimSpace(s)
}

And an important psychological effect: after this, code review is about meaning, not about “why is that curly brace lonely in this life.”

4. Step 2: tests (go test) and calmly reading the output

Tests are your way to keep confidence that the program’s behavior hasn’t broken. In Go this is especially important, because the language and its evolution rely heavily on testing as the main tool for detecting incompatibilities and unexpected breakages.

We’ll run tests across the entire project:

go test ./...

./... means “the current directory and all subpackages recursively” — it’s the most common mode for a project.

Mini example from our tasker

Suppose we have a function that builds a line to print a task in a list:

package task

type Task struct {
	Title string
	Done  bool
}

func Line(t Task) string {
	if t.Done {
		return "[x] " + t.Title
	}
	return "[ ] " + t.Title
}

And a test:

package task

import "testing"

func TestLine(t *testing.T) {
	if got := Line(Task{Title: "buy milk", Done: false}); got != "[ ] buy milk" {
		t.Fatalf("got %q", got)
	}
}

If tests pass, you’ll see something like:

ok  	example.com/tasker/task	0.003s
ok  	example.com/tasker/cmd/tasker	0.002s

If a test fails, go test usually shows the key things: which test, where it failed, and your message. For example, you accidentally broke the space after ]:

func Line(t Task) string {
	if t.Done {
		return "[x]" + t.Title // bug: missing space
	}
	return "[ ]" + t.Title // bug: missing space
}

Then the output will look like:

--- FAIL: TestLine (0.00s)
    task_test.go:7: got "[ ]buy milk"
FAIL
FAIL	example.com/tasker/task	0.003s

Read it in this order: first the line --- FAIL: ..., then file and line (task_test.go:7), then your message (got ...). The rest is “boilerplate.”

Important note about Example tests

We discussed earlier that in Go, examples in documentation can be executable and verifiable: Example... functions in _test.go with a // Output: comment are actually run by go test, and that helps keep documentation honest. In today’s gate that’s also a plus: there’s less chance an example gets stale and starts lying.

5. Step 3: go vet and suspicious spots in code

After tests, it’s time for go vet. It’s a static analyzer that looks for suspicious constructs. The key word here is “suspicious”: vet doesn’t guarantee it found a real bug, but its signals are usually worth attention, because they’re about potential defects rather than style.

Running it is straightforward:

go vet ./...

Classic example: Printf and format mismatches

In cmd/tasker/main.go we decided to print the number of tasks:

package main

import "fmt"

func main() {
	title := "42"
	fmt.Printf("%d\n", title) // error: %d expects a number, but we passed a string
}

The compiler may let this through because Printf takes ...any. But go vet will usually complain with something like this (the wording may differ, but the meaning is the same):

cmd/tasker/main.go:8:2: fmt.Printf format %d has arg title of wrong type string

How to fix it: either change the placeholder to %s, or convert the string to a number (if it really is a number), but conversion is separate logic with error handling, and doing it “just to silence vet” is a bad idea.

How to treat vet messages

The most useful habit: treat vet as a “high-risk” signal. Not always a “blocker,” but often closer to a bug than to bikeshedding. If vet complains, your first question should not be “how do I turn it off,” but “why is this suspicious, and what am I doing wrong.”

6. Step 4: linters and reading tool output

Linters are not a single tool, but a whole class. In Go you’ll often see staticcheck or the golangci-lint aggregator (which runs a set of analyzers). We won’t dive into the “linter zoo,” but we’ll lock in their role in the quality gate: linters help keep a project maintainable, highlight problematic patterns, and make code less prone to “slow decay.”

How you run them depends on what the project uses. For example:

staticcheck ./...

or:

golangci-lint run

Mini example: error text style as a maintainability signal

Suppose in the task package you introduced an error:

package task

import "errors"

var ErrBadInput = errors.New("Bad input.")

From the compiler’s perspective everything is fine. But many linters will highlight that in Go, error messages are conventionally written in lowercase, without a period, without “Error:”. That’s not because “an ancient gopher decreed it,” but because errors often get wrapped with context, and you want the final message chain to read like one sentence.

A normal version:

var ErrBadInput = errors.New("bad input")

It’s important to prioritize correctly: if tests are red, tests first. If vet found a Printf error, that comes first. Linters almost always come after.

How to read tool output: “where,” “what,” “how urgent”

When you first see output from go test/vet/a linter, it can feel like a stream of symbols. But if you break it down into a template, it becomes calmer. Almost always, a message consists of three parts: location (file:line:column), diagnosis (what’s wrong), and sometimes a recommendation/rule code.

Here’s a handy “how to interpret” table (not a law of nature, but a good practical model):

Tool Signal type Usually this is… What we do first
gofmt/goimports
noise/stabilization formatting, imports just apply it, don’t argue
go test
blocker broken behavior or test compilation fix immediately
go vet
high risk potential defect almost always fix right after tests
lint
style/maintainability (sometimes risk) bad patterns, agreements fix after blockers and vet

And now a small technique for reading file:line: open the file, jump to the line, look at 5–10 lines around it. Almost always the real problem is clearer in context than in a single line of output.

7. Mini scenario: run the gate and fix things step by step

You really want the quality gate to be not theory, but the feeling of “I can do this.” So let’s assemble a realistic scenario: you touched the task output function, added a small “improvement,” and everything went sideways.

We made a change and accidentally added a bug

package task

func Line(t Task) string {
	if t.Done {
		return "[x]" + t.Title // forgot the space
	}
	return "[ ]" + t.Title
}

Run gofmt/goimports

Suppose formatting doesn’t change meaning, but it stabilizes the code. You do:

gofmt -w .
goimports -w .

At this step we don’t “check,” we bring the project into the standard shape.

Run go test ./... and get a red signal

The test fails, and that’s a blocker. You fix exactly that, rather than arguing about error message style.

Fix:

func Line(t Task) string {
	if t.Done {
		return "[x] " + t.Title
	}
	return "[ ] " + t.Title
}

Run go vet ./... and catch a suspicious Printf

In parallel, somewhere in main.go you did:

package main

import "fmt"

func main() {
	count := "10"
	fmt.Printf("%d\n", count) // vet won't like this
}

Fix it:

fmt.Printf("%s\n", count) // print a string as a string
// 10

(Yes, sometimes the fix is just an honest %s.)

Run the linter and align with agreements

At that point, some “cosmetics” may surface that you can correct. For example, error style:

var ErrInvalidTitle = errors.New("invalid title") // ok by style

After that, you end up in a state where the project doesn’t just “somehow build,” but passes a minimal quality standard.

8. Common mistakes when running fmt/test/vet/lint

Mistake #1: run everything “in random order” and then drown in the output.
If you run checks in a random order, you’re almost guaranteed to fix the wrong thing at the wrong time. As a result, some changes will be unnecessary, some will conflict, and your brain will start hating the tools. The cure is simple: lock in the order fmt → test → vet → lint and repeat it the same way every time, like brushing your teeth (just without toothpaste on the keyboard).

Mistake #2: start with the linter when tests are red.
Linters can be chatty, and to a beginner it seems like “if there are more of them, they must be more important.” In practice it’s the opposite: red tests are a direct signal that behavior is broken. Until tests are green, it’s too early to discuss “improving readability”: first put out the fire, then pick the curtain color.

Mistake #3: treat go vet as style nitpicking.
vet is rarely about style; it’s more often about “oops, this can lead to unexpected behavior.” Stories with Printf formats are especially common. If you ignore vet, you’re stockpiling time bombs: they don’t always explode right away, but when they do, it’s maximally unpleasant.

Mistake #4: “fix without understanding,” just to make the message go away.
When a tool complains, it’s easy to slip into “remove the red stuff” mode. But if you don’t understand the rule’s meaning, you often fix the symptom, not the cause, and a week later you get a similar issue somewhere else. A healthy strategy: first understand why it’s a signal, then fix the code so it becomes more obvious and predictable.

Mistake #5: enable dozens of linters at once and declare war on your own project.
If you enable a huge set of checks without a policy, you get lots of noise and little value. Developers start turning rules off randomly or ignoring output entirely. It’s much healthier to start with a small set (for example, staticcheck and basic rules), train the team into the discipline, and only then expand.

1
Task
Go SELF, level 38, lesson 0
Locked
Input normalization
Input normalization
1
Task
Go SELF, level 38, lesson 0
Locked
Task line
Task line
1
Task
Go SELF, level 38, lesson 0
Locked
Task report
Task report
1
Task
Go SELF, level 38, lesson 0
Locked
Greeting service
Greeting service
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION