gofmt, go vet и golangci-lint: три уровня чистоты Go-кода
Go — один из немногих языков, где стиль форматирования не обсуждают: gofmt решает за вас. Но форматирование — лишь первый слой. go vet ловит скрытые ошибки компиляции, а golangci-lint объединяет десятки линтеров в один прогон. Разбираем, что именно проверяет каждый инструмент, чем они отличаются и как собрать из них рабочий пайплайн.

Зачем три инструмента, если есть gofmt
Новички в Go часто считают, что gofmt — это и есть «линтер». На деле gofmt отвечает только за внешний вид: табуляция, выравнивание, скобки. Он не знает, что вы передали sync.Mutex по значению вместо указателя и получите гонку данных. И уж точно не заметит, что в проекте из 40 пакетов три разных стиля обработки ошибок.
У каждого из трёх инструментов своя зона ответственности. Путать их — значит либо пропускать баги, либо дублировать проверки, замедляя CI.
gofmt: единый стиль без споров
gofmt входит в стандартную поставку Go и делает ровно одну вещь — приводит исходный код к каноническому форматированию. Табы вместо пробелов, фиксированные отступы, единообразные переносы строк. Инструмент не настраивается: у него нет конфигурационного файла и нет флагов, меняющих стиль. Это принципиальное решение авторов языка — убрать из код-ревью любые разговоры о форматировании.
gofmt vs goimports
goimports — расширение gofmt, которое дополнительно сортирует и добавляет или удаляет импорты. В большинстве проектов именно goimports используют как форматировщик по умолчанию, а чистый gofmt запускают разве что в минимальных контейнерах сборки.
Ключевой момент: gofmt и goimports не находят ошибок. Код после них выглядит одинаково, но может быть сколь угодно некорректным.
go vet: ловушки, которые компилятор пропускает
go vet — тоже часть стандартной поставки. Он выполняет статический анализ и ищет конструкции, которые формально компилируются, но почти наверняка содержат ошибку. Список проверок фиксирован и относительно невелик, зато каждая имеет очень низкий процент ложных срабатываний.
Что именно находит go vet
- printf — несоответствие формат-строки и аргументов:
fmt.Sprintf("%d", someString). - copylocks — копирование значений, содержащих
sync.Mutexилиsync.WaitGroup. - unreachable — код после
return,panicили бесконечного цикла. - structtag — синтаксически неверные или дублированные теги структур.
- loopclosure — захват переменной цикла в горутине (в Go 1.22+ поведение изменилось: переменная цикла теперь создаётся заново на каждой итерации, но vet всё равно предупреждает при компиляции под более ранние версии).
- shadow — затенение переменных (доступен через
go vet -vettoolс отдельным анализатором).
Полный перечень анализаторов можно посмотреть командой go tool vet help или в документации пакета golang.org/x/tools/go/analysis.
go vet спроектирован так, чтобы каждое предупреждение стоило исправить. Если инструмент сказал — правьте, не раздумывая. Это не рекомендация, а почти наверняка баг.
golangci-lint: агрегатор десятков линтеров
golangci-lint — сторонний проект, который запускает множество линтеров за один проход, переиспользуя общий парсинг AST. Без него пришлось бы ставить каждый линтер отдельно, запускать последовательно и вручную разбирать дублирующиеся результаты.
На момент написания (golangci-lint v1.62.x) инструмент поддерживает более 100 линтеров. По умолчанию включены около дюжины — остальные нужно активировать явно в конфигурации.
Линтеры, которые стоит включить первыми
| Линтер | Что ищет | Типичная находка |
|---|---|---|
| errcheck | Необработанные ошибки | os.Remove(path) без проверки err |
| staticcheck | Широкий набор: мёртвый код, ошибки API, устаревшие функции | Использование ioutil.ReadAll (deprecated с Go 1.16) |
| gocritic | Стилистические и производительные паттерны | append в цикле без предварительного make с capacity |
| gosec | Проблемы безопасности | Использование math/rand вместо crypto/rand для генерации токенов |
| unparam | Параметры функций, которые всегда принимают одно значение | Аргумент ctx, всегда равный context.Background() |
| bodyclose | Незакрытые тела HTTP-ответов | resp, _ := http.Get(url) без defer resp.Body.Close() |
Точный набор включённых по умолчанию линтеров и их версии меняются между релизами golangci-lint. Сверяйтесь с выводом golangci-lint linters для вашей версии.
Конфигурация: .golangci.yml
Минимальный рабочий файл выглядит так:
# .golangci.yml — golangci-lint v1.62.x
run:
timeout: 3m
linters:
enable:
- errcheck
- staticcheck
- gocritic
- gosec
- bodyclose
- unparam
linters-settings:
errcheck:
check-type-assertions: true
gocritic:
enabled-tags:
- diagnostic
- performance
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
Обратите внимание на timeout: на больших монорепозиториях стандартной минуты не хватает, и CI молча завершается по таймауту, создавая иллюзию чистого кода.
Как они работают вместе
golangci-lint уже включает в себя gofmt (как линтер gofmt) и все проверки go vet (как линтер govet). То есть при запуске golangci-lint с дефолтной конфигурацией вы получаете покрытие обоих стандартных инструментов плюс дополнительные линтеры.
Но это не значит, что gofmt и go vet можно выбросить из обихода:
- gofmt -w переписывает файлы. golangci-lint — только диагностирует. Форматирование нужно применять до коммита, а не в CI.
- go vet встроен в
go test(запускается автоматически перед тестами с Go 1.10+). Даже без golangci-lint в CI базовые проверки уже работают. - golangci-lint — тяжёлый инструмент. Локально разработчик может запускать его реже, а gofmt и go vet — на каждом сохранении через редактор.
Типичный пайплайн
- Pre-commit hook или настройка редактора — goimports при сохранении файла. VS Code с расширением Go делает это из коробки; в Neovim / GoLand настраивается аналогично.
- Pre-push hook (опционально) —
go vet ./..., занимает секунды даже на крупном проекте. - CI (GitHub Actions / GitLab CI) —
golangci-lint run ./...с конфигом из репозитория. Проверки gofmt и govet включены внутри, дублировать отдельными шагами не нужно.
Частая ошибка — включить golangci-lint в проект с большой кодовой базой и сразу активировать все линтеры. Результат: тысячи предупреждений, которые никто не будет исправлять, и в итоге линтер отключают. Практичнее начать с дефолтного набора, добавить 3–5 линтеров, зафиксировать baseline через issues.exclude-rules или флаг --new-from-rev (проверяет только код, изменённый после указанного коммита) и расширять покрытие постепенно.
Подводные камни
Ложные срабатывания
go vet почти не даёт false positive. У golangci-lint ситуация другая: некоторые линтеры (например, wrapcheck, exhaustruct) очень агрессивны и генерируют шум, особенно в кодовых базах с кодогенерацией. Решение — подавлять предупреждения не глобальным отключением линтера, а директивой //nolint:имялинтера с обязательным комментарием, зачем.
Версии Go и совместимость
golangci-lint жёстко привязан к версии Go: релиз golangci-lint собирается под конкретный диапазон версий компилятора. Если в CI стоит Go 1.23, а golangci-lint собран для Go 1.21 — возможны ошибки парсинга нового синтаксиса. Всегда обновляйте оба инструмента вместе и фиксируйте версию golangci-lint в CI, а не ставьте latest.
Кодогенерация
Сгенерированные файлы (protobuf, ent, sqlc и т.д.) засоряют отчёт. Стандартный способ — комментарий // Code generated ... DO NOT EDIT. в начале файла: golangci-lint пропускает такие файлы автоматически. Если генератор не ставит эту строку, добавьте паттерн в секцию issues.exclude-dirs или run.skip-files конфигурации.
Что делать дальше
После настройки базового пайплайна имеет смысл посмотреть в сторону staticcheck как отдельного инструмента — он развивается быстрее, чем обёртка внутри golangci-lint, и иногда содержит проверки, которых в текущей версии агрегатора ещё нет. Второй шаг — подключить линтер revive как замену устаревшему golint для стилистических правил, специфичных для вашей команды. И наконец, если вы пишете библиотеку — включите gocritic с тегом experimental: там бывают полезные находки, которые в стабильном наборе появятся только через пару релизов.
Версия Go, для которой написан материал: 1.22–1.23. Версия golangci-lint: 1.62.x. Поведение отдельных линтеров может отличаться в более новых релизах — сверяйтесь с официальной документацией.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: golangci-lint — официальная документация, Go command — vet (официальная документация Go)
Частые вопросы
Зачем запускать gofmt отдельно, если golangci-lint тоже проверяет форматирование?
golangci-lint выдаёт сотни ошибок в уже работающем проекте — как начать без боли?
go vet запускается автоматически при go test?
Что если golangci-lint не поддерживает мою версию Go?
Можно ли подавить предупреждение конкретного линтера в одной строке?
Чем staticcheck отличается от go vet?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.