JavaRush /Courses /Go SELF /go vet — error classes: printf, shadow, and friends

go vet — error classes: printf, shadow, and friends

Go SELF
Level 30, Lesson 2
Available

1. Where go vet fits in the code quality process

The Go compiler is a strict guy: if types don’t match or the syntax is off, it simply won’t build your program. But there’s another class of problems: code that is formally correct, compiles, and sometimes even “seems to work,” but hides a mismatch in expectations inside. go vet is exactly about such cases: it looks at your code and says, “I think you’re doing something weird here — double-check that this is really what you meant.”

You can think of go vet as an attentive review buddy who never gets tired, doesn’t get distracted by coffee, and doesn’t leave comments like “you know what I mean.” It doesn’t replace tests and doesn’t prove your program correct, but it catches lots of typical “almost bugs” before runtime — saving you hours of life (and a couple of nerve cells).

By the way, historically vet used to be a separate tool, and then it was “packaged” under the go command, like many other utilities in the Go ecosystem.

Where it sits in the verification chain

Once you start writing code that’s “a bit more serious than one 20-line file,” a normal engineering need appears: agree on what counts as the minimum acceptable level of quality. We already cover formatting and imports with tools (for this day — gofmt and goimports), and go vet naturally becomes the next layer: it checks whether there are constructs that often indicate a mistake.

A convenient mental model is: formatting makes the code readable, vet catches suspicious spots, and tests confirm behavior in real scenarios.

flowchart LR
    A["Code written"] --> B["Formatting (gofmt/goimports)"]
    B --> C["Static analysis (go vet)"]
    C --> D["Tests (go test)"]
    D --> E["No need to be ashamed (within reason)"]

It’s important to understand the boundary: go vet is static analysis, meaning it doesn’t run your program — it analyzes the source code. It doesn’t know “what will really happen in prod,” but it can find patterns that often end in bugs.

How to run go vet and read its messages

At this stage of the course, it’s enough for you to be able to run go vet on a project and read the output without panicking. Usually you run it on a package or on all packages at once. In a terminal, this is often done with a command like go vet ./..., and in an IDE there’s usually a menu item that does the same thing (but with a button, which, let’s be honest, is nice).

vet messages usually look like “in file X at line Y there’s a suspicious construct Z.” This is not a verdict; it’s a signal: stop, understand the intent, and either fix it or consciously keep it as-is (rare, but it happens). The most common beginner mistake is to see a message and think, “eh, later.” No — it’s like a “check engine” light: the car might still be driving, but it’s better not to pretend it doesn’t exist.

What an “error class” means in go vet

Inside go vet there’s a set of separate checks — it’s convenient to think of them as “analyzers” (analyzer). Each analyzer looks for its own type of problems: one checks Printf format strings, another checks weird struct tags, a third checks variable shadowing, and so on.

Why is it important to think in terms of “classes”? Because you start recognizing patterns. If you understand the printf class, you’ll often suspect the issue even before vet: “okay, %d and a string… this is going to end sadly.” And once you know the structtag class, you stop treating tags like “magic spells” and start seeing them as ordinary strings with a strict format.

Below we’ll cover the most useful classes for your current level: printf, the string(int) conversion, shadow, structtag, “unnamed struct literals from another package” (composites) and unusedresult.

3. The printf class: the format doesn’t match the arguments

fmt.Printf is a very powerful thing, but it comes with a price: it accepts arguments as ...any. That means the compiler cannot strictly verify that your %d actually received a number, and your %s actually received a string. And that’s where a favorite bug genre appears: “everything compiles, but the output is weird… or there’s a panic… or the data got messed up.”

Type mismatch: %d and a string

Look at this example — it compiles, but it’s obviously suspicious:

package main

import "fmt"

func main() {
	age := "10"
	fmt.Printf("age=%d\n", age) // wanted a number, got a string
}

The fix depends on the intent. If age is a string, print it with %s. If it’s a “number that came in as a string,” parse it with strconv.Atoi and print it with %d.

Argument count mismatch

Another typical fail is forgetting an argument or adding an extra one:

package main

import "fmt"

func main() {
	name := "Bob"
	fmt.Printf("hello %s %d\n", name) // %d did not get an argument
}

go vet loves catching this because it’s almost always a mistake.

A mini example in our learning app

Let’s imagine we have a simple task model:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
}

func main() {
	t := Task{ID: 7, Title: "Buy milk"}
	fmt.Printf("task id=%s title=%s\n", t.ID, t.Title) // printing ID as a string
}

Correct:

fmt.Printf("task id=%d title=%s\n", t.ID, t.Title) // task id=7 title=Buy milk

Notice: we don’t “force” the data to fit the format. We choose the format to match the data. It’s like shoes: you can try to put sneakers on your head, sure, but it’s better the other way around.

4. The string(int) class: why is string(10) not "10"?

Many beginners honestly think: “well, it’s an int, so I can turn it into a string.” In Go you can write string(n), and it will compile, but the meaning is not “a number as text,” it’s “the Unicode character with code point n.” For example, string(65) yields "A".

This construct exists for historical reasons, but it confuses people so consistently that diagnostic checks were added for it in vet, because it’s a classic trap.

Trap demonstration

package main

import "fmt"

func main() {
	n := 65
	fmt.Println(string(n)) // A
}

If you wanted "65", you need strconv.Itoa(n):

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n := 65
	fmt.Println(strconv.Itoa(n)) // 65
}

How it shows up in a “task tracker”

A very realistic situation: you print a task ID by building a string:

package main

import "fmt"

func main() {
	id := 10
	fmt.Println("task " + string(id)) // surprise: "task \n"
}

10 as a character code is a newline, so you’ll get a line break instead of “10”. And if you look at it and think “wow, some magic,” congratulations: you’ve just met the language’s very normal, strict semantics.

5. The shadow class: variable shadowing and the danger of :=

Shadowing is when you create a new variable with the same name in an inner block, and it “hides” the outer one. Formally it’s correct, but it’s easy to end up in the situation “I thought I updated the variable, but actually I created a new one and updated that.”

Yes, we already discussed shadowing earlier, but here the practical point matters: go vet (and some other tools) can highlight these places, especially when they look like a bug.

It’s also useful to remember: shadowing often happens because of :=. That’s exactly why in Go you’re advised to treat := as a great tool, but not as “use it everywhere you don’t feel like thinking.”

By the way, a similar problem is described in the context of a typical mistake: “accidentally shadowed a slice variable, then kept using the old one.”

Mini example: “the error got lost”

package main

import (
	"errors"
	"fmt"
)

func main() {
	var err error

	if true {
		err := errors.New("boom") // created a NEW err
		_ = err
	}

	if err == nil {
		fmt.Println("all good") // all good (but didn’t we do boom?)
	}
}

The fix is to replace := with = where you want to update an existing variable:

if true {
	err = errors.New("boom") // updated the outer err
}

Shadowing in argument parsing code

Let’s imagine we have a function that parses a task ID from a string:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	idStr := "42"
	id := 0

	if n, err := strconv.Atoi(idStr); err == nil {
		id := n // shadowed id; the outer one stayed 0
		_ = id
	}

	fmt.Println(id) // 0
}

This is a logical error: you wanted to assign n to the outer id, but you wrote := and created a new id inside the if. Fix:

if n, err := strconv.Atoi(idStr); err == nil {
	id = n
}

6. The structtag class: “it’s just a string… why did it break?”

Struct tags look like magic: backticks, and inside them a mini-language. But technically it’s just a string literal that third-party libraries read according to certain rules. If you mess up the format, the compiler will often stay silent (tags don’t directly affect execution), but the library may start behaving differently than you expect.

That’s why go vet checks suspicious tags — for example, when the format looks wrong. This is especially relevant after you’ve already seen tags like json:"name,omitempty" and understand that the comma and quotes are not there for decoration.

An example that’s “almost right, but not”

package main

type Task struct {
	ID int `json: "id"` // the space and colon are in the wrong place: looks suspicious
}

func main() {}

Correct:

package main

type Task struct {
	ID int `json:"id"`
}

func main() {}

An example closer to our app

Let’s say we have a task model in the model package:

package model

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

Yes, we’re not serializing tasks to JSON yet (that will be a separate big topic), but the tags can already live in the model as a “contract for the future.” The key is that they must be correct; otherwise, later you’ll get a fun bug: “why doesn’t this field parse,” and the culprit will be a space in a tag from the past.

7. The composites class: positional struct literals from another package

In Go you can create a struct positionally: Task{1, "Title", false}. It works, but it has one downside: if you add a field or change the order, old code will either stop compiling or (worse) will compile but change meaning.

go vet really likes to complain about unnamed (unkeyed) struct literals from another package. Why? Because it’s considered a brittle API contract: you depend on the field order of someone else’s type, and that’s not a commitment you want to make.

An example with our model and app packages

Suppose in model:

package model

type Task struct {
	ID    int
	Title string
	Done  bool
}

And in app you create a task like this:

package app

import "example.com/tasker/model"

func NewDemo() model.Task {
	return model.Task{1, "Read Go book", false} // positional
}

Better to write it with field names:

return model.Task{
	ID:    1,
	Title: "Read Go book",
	Done:  false,
}

It’s a couple lines longer, but it’s more readable and more resilient to struct changes.

8. The unusedresult class: “I called a function… why did nothing happen?”

There are functions that return a result, and if you ignore it, it’s usually a mistake. For example, fmt.Sprintf does not print; it returns a string. Beginners sometimes expect “well, it’s fmt, so it printed.” No, it didn’t. A string was born and died alone.

go vet can highlight ignored results for some functions, because it’s a classic “you probably confused Sprintf and Printf.”

Example: Sprintf without using the result

package main

import "fmt"

func main() {
	fmt.Sprintf("hello %s", "Bob") // the string is created and thrown away
	fmt.Println("done")            // done
}

Correct options depend on the goal:

If you wanted to print:

fmt.Printf("hello %s\n", "Bob") // hello Bob

If you wanted to get a string:

s := fmt.Sprintf("hello %s", "Bob")
fmt.Println(s) // hello Bob

A similar mistake in a “task tracker”

package main

import "fmt"

func main() {
	id := 3
	fmt.Sprintf("selected task id=%d", id) // forgot to use it
}

If the goal is to show it to the user, use Printf/Println.

9. A mini “correct” fragment for the app

Let’s assemble a small piece of code that looks “friendly” to vet: the format matches, there are no string(int) traps, no weird tags, no positional literals from another package, and no shadowing.

package main

import (
	"fmt"
	"strconv"
)

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

func main() {
	idStr := "7"

	id, err := strconv.Atoi(idStr)
	if err != nil {
		fmt.Printf("bad id=%q\n", idStr) // bad id="..."
		return
	}

	t := Task{ID: id, Title: "Buy milk", Done: false}
	fmt.Printf("task id=%d title=%s done=%v\n", t.ID, t.Title, t.Done)
	// task id=7 title=Buy milk done=false
}

The point here is not that this is “perfect code.” The point is that it’s predictable: every expression does exactly what it says, and the quality tools will stay silent — and the silence of tools is, oddly enough, sometimes the nicest sound in development.

10. Common mistakes when working with go vet

Mistake #1: treating go vet as “another compiler” and being afraid of every warning.
vet does not promise 100% accuracy: it says “this looks like a bug.” Your job is to understand why it looks like a bug and make a decision. In learning projects, the right answer is almost always to fix it, because warnings typically point to a genuinely incorrect expectation.

Mistake #2: fixing a warning mechanically without understanding it.
For example, seeing “printf: wrong type” and just changing %d to %v. Yes, the message will go away, but you may have lost an important idea: you’re printing a string as a number or a number as a string, and that’s a symptom of a wrong data model. It’s better to pause for a second and ask yourself: “what type should this really be?”

Mistake #3: not noticing string(int) and trying to “fix” output with hacks.
When you see "task \n" instead of "task 10", you don’t need to write “if the character is weird, print differently.” You need to understand: string(int) is a character by code, not a number. After that, calmly switch to strconv.Itoa. The reason analyzers flag this at all is that this mistake is unbelievably common among beginners.

Mistake #4: ignoring variable shadowing because “it still compiles.”
Shadowing is one of the sneakiest bugs: the outer variable keeps its old value, while you’re sure you updated it. This is especially painful in error-handling code (err) and in code where values are expected to change after an if. If you see a suspicious := inside a block, it’s a great reason to check whether you meant =.

Mistake #5: keeping positional struct literals “because it’s shorter.”
Yes, it’s shorter. But the price is brittleness. As soon as the struct changes (and structs almost always change), you either break the build or get a silent bug. And this is where vet works like a vaccine: a little discipline now beats investigating “why did Done suddenly become Title” later.

1
Task
Go SELF, level 30, lesson 2
Locked
Output format
Output format
1
Task
Go SELF, level 30, lesson 2
Locked
Task number
Task number
1
Task
Go SELF, level 30, lesson 2
Locked
Selected identifier
Selected identifier
1
Task
Go SELF, level 30, lesson 2
Locked
Vet without noise
Vet without noise
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION