1. Место go vet в процессе качества кода
Компилятор Go — парень строгий: если типы не сходятся или синтаксис кривой, он просто не даст собрать программу. Но есть другой класс проблем: код формально корректный, компилируется и даже иногда «как будто работает», но внутри спрятана ошибка ожиданий. go vet как раз про такие случаи: он смотрит на код и говорит «мне кажется, вы сейчас делаете странную штуку — проверьте, точно ли вы это имели в виду».
Можно думать о go vet как о внимательном напарнике на ревью, который не устает, не отвлекается на кофе и не пишет комментарий «ну ты понял». Он не заменяет тесты и не доказывает корректность программы, но ловит много типовых «почти багов» до запуска — и этим экономит вам часы жизни (и пару нервных клеток).
Кстати, исторически vet был отдельным инструментом, а затем его «упаковали» под команду go, как и многие другие утилиты Go‑экосистемы.
Где он стоит в цепочке проверки
Когда вы начинаете писать код «чуть серьёзнее, чем один файл на 20 строк», возникает нормальная инженерная потребность: договориться, что считать минимально приемлемым качеством. Форматирование и импорты мы уже закрываем инструментами (в рамках этого дня — gofmt и goimports), а go vet логично ставится следующим слоем: он проверяет, нет ли конструкций, которые часто означают ошибку.
Удобная ментальная схема такая: форматирование делает код читаемым, vet ловит подозрительные места, а тесты подтверждают поведение на сценариях.
flowchart LR
A["Код написан"] --> B["Форматирование (gofmt/goimports)"]
B --> C["Статический анализ (go vet)"]
C --> D["Тесты (go test)"]
D --> E["Можно не стыдиться (в пределах разумного)"]
Важно понять границу: go vet — это статический анализ, то есть он не запускает вашу программу, а анализирует исходники. Он не знает «как реально будет на проде», но умеет находить закономерности, которые часто заканчиваются багами.
Как запускать go vet и читать его сообщения
На этом этапе курса вам достаточно уметь запускать go vet на проект и прочитать результат без паники. Обычно запуск выглядит как проверка пакета или всех пакетов сразу. В терминале это часто делают командой вида go vet ./..., а в IDE обычно есть пункт меню, который делает то же самое (только кнопкой, что, будем честны, приятно).
Сообщения vet обычно похожи на «в файле X на строке Y подозрительная конструкция Z». Это не приговор, а сигнал: остановиться, понять смысл и либо исправить, либо осознанно оставить как есть (редко, но бывает). У новичков самая частая ошибка — видеть сообщение и думать «ну ладно, потом». Нет, это как лампочка «check engine»: машина, может, ещё едет, но лучше не делать вид, что её не существует.
Что значит «класс ошибок» в go vet
У go vet внутри есть набор отдельных проверок — их удобно представлять как «анализаторы» (analyzer). Каждый анализатор ищет свой тип проблем: один — форматные строки Printf, другой — странные struct tags, третий — затенение переменных (shadowing), и так далее.
Почему нам важно думать именно «классами»? Потому что вы начинаете узнавать паттерны. Если вы понимаете класс printf, вы начинаете автоматически подозревать проблему ещё до vet: «так, %d и строка… это точно закончится грустно». А когда вы знаете класс structtag, вы перестаёте относиться к тегам как к «магическим заклинаниям» и начинаете видеть в них обычные строки со строгим форматом.
Ниже мы разберём самые полезные для вашего текущего уровня классы: printf, string(int)‑конверсию, shadow, structtag, «неименованные литералы структур из другого пакета» (composites) и unusedresult.
3. Класс printf: формат не совпал с аргументами
fmt.Printf — очень мощная штука, но у неё есть цена: она принимает аргументы как ...any. То есть компилятор не может жёстко проверить, что ваш %d действительно получил число, а %s действительно получил строку. И вот тут появляется любимый жанр багов: «всё компилируется, но вывод странный… или вообще паника… или данные поехали».
Несовпадение типа: %d и строка
Посмотрите на пример — он компилируется, но очевидно подозрительный:
package main
import "fmt"
func main() {
age := "10"
fmt.Printf("age=%d\n", age) // хотели число, дали строку
}
Правка зависит от смысла. Если age — строка, печатаем %s. Если это «число, пришедшее строкой», то парсим через strconv.Atoi и печатаем %d.
Несовпадение количества аргументов
Ещё один типовой провал — забыли аргумент или добавили лишний:
package main
import "fmt"
func main() {
name := "Bob"
fmt.Printf("hello %s %d\n", name) // %d не получил аргумент
}
go vet такие вещи любит ловить, потому что это почти всегда ошибка.
Мини‑пример в нашем учебном приложении
Представим, что у нас есть простая модель задачи:
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) // ID печатаем как строку
}
Правильно так:
fmt.Printf("task id=%d title=%s\n", t.ID, t.Title) // task id=7 title=Buy milk
Обратите внимание: мы не «подгоняем» данные под формат. Мы выбираем формат под данные. Это как обувь: можно, конечно, попытаться надеть кроссовки на голову, но лучше всё-таки наоборот.
4. Класс string(int): почему string(10) — это не "10"?
Многие новички честно думают: «ну раз int, значит можно превратить в string». В Go можно написать string(n), и код скомпилируется, но смысл будет не «число в виде текста», а «символ Unicode с кодом n». Например, string(65) даст "A".
Эта конструкция исторически существует, но она путает людей настолько регулярно, что для неё даже добавляли диагностические проверки в vet, потому что это классическая ловушка.
Демонстрация ловушки
package main
import "fmt"
func main() {
n := 65
fmt.Println(string(n)) // A
}
Если вы хотели "65", нужен strconv.Itoa(n):
package main
import (
"fmt"
"strconv"
)
func main() {
n := 65
fmt.Println(strconv.Itoa(n)) // 65
}
Как это проявляется в «задачнике»
Очень жизненная ситуация: вы печатаете ID задачи, собирая строку:
package main
import "fmt"
func main() {
id := 10
fmt.Println("task " + string(id)) // внезапно: "task \n"
}
10 как код символа — это перевод строки, поэтому вы получите перенос, а не «10». И если вы смотрите на это и думаете «ну да, магия какая-то», то поздравляю: вы только что встретились с самой обычной строгой семантикой языка.
5. Класс shadow: затенение переменных и опасность :=
Затенение (shadowing) — это когда вы создаёте новую переменную с тем же именем во внутреннем блоке, и она «закрывает» внешнюю. Формально всё корректно, но легко получить ситуацию «я думал, что обновил переменную, а на самом деле создал новую и обновил её».
Да, мы уже обсуждали shadowing раньше, но здесь важен практический аспект: go vet (и некоторые другие инструменты) умеют подсвечивать такие места, особенно когда они выглядят как ошибка.
Отдельно полезно помнить: затенение часто случается из‑за :=. Это ровно та причина, по которой в Go советуют относиться к := как к отличному инструменту, но не как к «ставь везде, где лень думать».
Кстати, похожая проблема описывается и в контексте типичной ошибки «случайно затенили переменную слайса, а потом продолжили использовать старую».
Мини‑пример: «ошибка потерялась»
package main
import (
"errors"
"fmt"
)
func main() {
var err error
if true {
err := errors.New("boom") // создали НОВУЮ err
_ = err
}
if err == nil {
fmt.Println("всё ок") // всё ок (но мы же вроде делали boom?)
}
}
Правка — заменить := на = там, где вы хотите обновить уже существующую переменную:
if true {
err = errors.New("boom") // обновили внешнюю err
}
Shadowing в коде парсинга аргументов
Представим, что у нас есть функция, которая парсит ID задачи из строки:
package main
import (
"fmt"
"strconv"
)
func main() {
idStr := "42"
id := 0
if n, err := strconv.Atoi(idStr); err == nil {
id := n // затенили id, внешний остался 0
_ = id
}
fmt.Println(id) // 0
}
Тут ошибка логическая: вы хотели записать n во внешний id, но написали := и создали новый id внутри if. Исправление:
if n, err := strconv.Atoi(idStr); err == nil {
id = n
}
6. Класс structtag: «это же просто строка… почему она сломалась?»
Struct tags выглядят как магия: какие-то обратные кавычки, внутри — мини‑язык. Но технически это просто строковый литерал, который сторонние библиотеки читают по определённым правилам. Если вы ошиблись в формате, компилятор часто промолчит (теги не влияют на выполнение напрямую), а вот библиотека может начать работать не так, как вы ожидали.
Поэтому go vet проверяет подозрительные теги: например, когда формат выглядит неправильным. Это особенно актуально после того, как вы уже видели теги вида json:"name,omitempty" и понимаете, что запятая и кавычки там не для красоты.
Пример «почти как надо, но нет»
package main
type Task struct {
ID int `json: "id"` // пробел и двоеточие не там: выглядит подозрительно
}
func main() {}
Правильно:
package main
type Task struct {
ID int `json:"id"`
}
func main() {}
Пример ближе к нашему приложению
Пусть у нас модель задачи в пакете model:
package model
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
Да, мы пока не сериализуем задачи в JSON (это будет отдельная большая тема), но теги уже могут лежать в модели как «контракт на будущее»: главное — чтобы они были корректными, иначе в будущем вы получите весёлый баг «почему поле не парсится», а виноват пробел в теге из прошлого.
7. Класс composites: позиционные литералы структур из другого пакета
В Go можно создавать структуру позиционно: Task{1, "Title", false}. Это работает, но имеет один минус: если вы добавите поле или поменяете порядок, то старый код либо перестанет собираться, либо (хуже) соберётся, но смысл изменится.
go vet очень любит ругаться на неименованные (unkeyed) литералы структур из другого пакета. Почему? Потому что это считается хрупким API‑контрактом: вы зависите от порядка полей чужого типа, а это не то, на что приятно подписываться.
Пример на наших пакетах model и app
Допустим, в model:
package model
type Task struct {
ID int
Title string
Done bool
}
А в app вы создаёте задачу так:
package app
import "example.com/tasker/model"
func NewDemo() model.Task {
return model.Task{1, "Read Go book", false} // позиционно
}
Лучше писать с именами полей:
return model.Task{
ID: 1,
Title: "Read Go book",
Done: false,
}
Это дольше на пару строк, но зато читабельнее и устойчивее к изменениям структуры.
8. Класс unusedresult: «я вызвал функцию… почему ничего не произошло?»
Есть функции, которые возвращают результат, и если вы его игнорируете, то чаще всего это ошибка. Например, fmt.Sprintf не печатает, она возвращает строку. Новички иногда ожидают «ну раз fmt, значит вывелось». Нет, не вывелось. Просто строка родилась и умерла в одиночестве.
go vet умеет подсвечивать игнорирование результата у некоторых функций, потому что это прямо классическое «кажется, вы перепутали Sprintf и Printf».
Пример: Sprintf без использования результата
package main
import "fmt"
func main() {
fmt.Sprintf("hello %s", "Bob") // строка создана и выброшена
fmt.Println("done") // done
}
Правильные варианты зависят от цели:
Если вы хотели вывести:
fmt.Printf("hello %s\n", "Bob") // hello Bob
Если вы хотели получить строку:
s := fmt.Sprintf("hello %s", "Bob")
fmt.Println(s) // hello Bob
Похожая ошибка в «задачнике»
package main
import "fmt"
func main() {
id := 3
fmt.Sprintf("selected task id=%d", id) // забыли использовать
}
Если цель — показать пользователю, делаем Printf/Println.
9. Мини‑сборка «правильного» фрагмента для приложения
Соберём маленький кусочек кода, который выглядит «дружелюбно» к vet: формат совпадает, нет string(int)‑ловушек, нет странных тегов, нет позиционных литералов из другого пакета и нет затенения.
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
}
Здесь важно не то, что это «идеальный код». Важно, что он предсказуем: каждое выражение делает ровно то, что написано, и инструменты качества будут молчать — а молчание инструментов, как ни странно, иногда самый приятный звук в разработке.
10. Типичные ошибки при работе с go vet
Ошибка №1: воспринимать go vet как «ещё один компилятор» и бояться каждого предупреждения.
vet не обещает стопроцентную точность: он говорит «похоже на баг». Ваша задача — понять, почему это похоже на баг, и принять решение. В учебных проектах почти всегда правильный ответ — исправить, потому что предупреждения обычно указывают на реально неверное ожидание.
Ошибка №2: исправлять предупреждение механически, не поняв смысл.
Например, увидеть «printf: wrong type» и просто поменять %d на %v. Да, сообщение исчезнет, но вы могли потерять важную идею: вы печатаете строку как число или число как строку, а это симптом неправильной модели данных. Лучше на секунду остановиться и спросить себя: «какой тип тут должен быть на самом деле?»
Ошибка №3: не замечать string(int) и пытаться «починить» вывод костылями.
Когда вы видите "task \n" вместо "task 10", не нужно писать «если символ странный — печатать по-другому». Нужно понять: string(int) — это символ по коду, а не число. И после этого спокойно перейти на strconv.Itoa. Причина, почему это вообще подсвечивают анализаторы, в том, что такая ошибка невероятно типична у новичков.
Ошибка №4: игнорировать затенение переменных, потому что «всё же компилируется».
Shadowing — один из самых коварных багов: внешняя переменная остаётся со старым значением, а вы уверены, что её обновили. Особенно это больно в коде с ошибками (err) и с изменением значений после if. Если вы видите подозрительный := внутри блока — это отличный повод проверить, не хотели ли вы =.
Ошибка №5: хранить позиционные литералы структур «потому что так короче».
Да, короче. Но цена — хрупкость. Как только структура меняется (а структуры меняются почти всегда), вы либо ломаете сборку, либо получаете тихий баг. И вот тут vet работает как прививка: лучше немного дисциплины сейчас, чем расследование «почему Done внезапно стал Title» потом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ