1. Введение
Если вы только начинаете, кажется логичным: «компилятор же умный, он всё скажет». Проблема в том, что компилятор обязан быть честным формалистом: если код корректен по правилам языка — он пропускает. Но корректный код может быть странным, хрупким или просто подозрительным. go vet добавляет официальный слой «похоже на ошибку», а линтеры расширяют эту идею: они находят больше паттернов, которые статистически часто оказываются багами или источниками боли в ревью.
Представьте, что компилятор — это охранник на входе в здание: «у вас пропуск есть? проходите». go vet — внимательный охранник, который ещё спрашивает: «а вы точно туда идёте?». А линтеры — коллега, который работает здесь давно и говорит: «слушай, ты можешь зайти, но вот здесь люди обычно падают в люк, будь осторожен». Иногда коллега ошибается, но чаще экономит вам часы.
Чтобы не путаться, полезно держать в голове разделение обязанностей:
| Инструмент | Что делает | Что не делает |
|---|---|---|
|
приводит код к каноническому формату | не чинит логику |
|
формат + импорты «как надо» | не лечит архитектуру зависимостей |
|
официальный статический анализ «похоже на баг» | не проверяет все возможные подозрения и стилистику |
| линтеры | расширенный анализ: подозрения + упрощения + стиль | не гарантируют истинность каждого предупреждения |
2. staticcheck: практичный статанализатор
staticcheck — это один из самых популярных линтеров в Go-мире, потому что он довольно практичный: многие его предупреждения реально указывают на баги или на код, который можно сделать проще без потери ясности. Важно психологически правильно его воспринимать: это не «экзаменатор», который ищет, к чему придраться, а инструмент, который пытается уменьшить количество глупых ошибок и сделать код более читаемым.
При этом staticcheck — не «официальный стандарт языка» в том же смысле, что gofmt или go vet. Он может быть более мнительным: иногда предупреждение можно проигнорировать, если вы понимаете, зачем написали именно так. Но ключевое слово тут — понимаете. Самый опасный сценарий новичка — не понимать предупреждение, но всё равно «замолчать» его, чтобы стало зелёненько.
Полезная ментальная модель: staticcheck часто ловит три категории вещей.
- «Подозрение на баг». Например, вы вычислили значение, присвоили переменной — и тут же перезаписали её, так и не использовав. Это часто след от неаккуратного рефакторинга.
- «Бессмысленный или избыточный код». Например, вы сравниваете bool с true, хотя можно написать проще (и мозгу легче читать).
- «Упрощения и идиомы». Это не всегда про «скорость», чаще — про читаемость: меньше веток, меньше шума, меньше мест, где можно случайно ошибиться.
3. Практика на примерах: мини таск-менеджер
Чтобы линтеры не воспринимались как магическая коробка, давайте посмотрим на примерах. Представим, что мы продолжаем учебное консольное приложение: простой таск-менеджер, который хранит задачи в памяти и печатает их. Мы не делаем здесь «идеальную архитектуру», нам важнее научиться видеть, почему линтер ругается, и как это улучшает код.
Сравнение bool с true: код корректный, но шумный
Когда вы только привыкаете к if, легко писать «в лоб»: if x == true. Это работает, но в Go так обычно не пишут — потому что читается тяжелее, чем должно.
package todo
type Task struct {
Title string
Done bool
}
func (t Task) IsDone() bool {
if t.Done == true {
return true
}
return false
}
Здесь всё корректно, но мысль «задача сделана?» утонула в лишних словах. Линтер, скорее всего, предложит упростить.
Нормальный, читабельный вариант:
package todo
type Task struct {
Title string
Done bool
}
func (t Task) IsDone() bool {
return t.Done
}
Почему это важно? Потому что через неделю вы откроете код и быстрее поймёте смысл. А ещё чем меньше строк, тем меньше мест, где можно случайно сделать глупость.
Бесполезное присваивание: классика после правок
Очень распространённая ситуация: вы что-то посчитали, потом передумали и «временно» поставили другое значение, а старое забыли удалить. Код компилируется, тестов может не быть, но логика уже не та.
package todo
func nextID(tasksCount int) int {
id := tasksCount + 1
id = 1 // случайно оставили "временную" строку
return id
}
Это выглядит почти невинно, но по факту tasksCount больше не влияет на результат. Если функция должна выдавать следующий ID — это уже баг поведения. staticcheck обычно очень любит такие места и подсвечивает их, потому что они часто «настоящие».
Исправление зависит от того, что вы хотели. Если вы реально хотите tasksCount + 1, то просто убираете мусор:
package todo
func nextID(tasksCount int) int {
id := tasksCount + 1
return id
}
Текст ошибки: важная привычка для экосистемы
Сами по себе сообщения об ошибках — часть UX для разработчика. В Go принято, чтобы текст ошибки был в нижнем регистре и не начинался с Error:. Многие линтеры подсветят вариант «с большой буквы», потому что потом такие ошибки плохо склеиваются в цепочки и некрасиво выглядят в логах.
Плоховатый вариант:
package todo
import "errors"
func ValidateTitle(title string) error {
if title == "" {
return errors.New("Title is empty")
}
return nil
}
Лучше:
package todo
import "errors"
func ValidateTitle(title string) error {
if title == "" {
return errors.New("title is empty")
}
return nil
}
Это мелочь, но такие мелочи в большом проекте экономят нервы. В реальном приложении сообщение можно сделать ещё человечнее, но сейчас нам важно уловить стиль.
Избыточная обёртка: когда код можно сделать прямее
Новички часто пишут «по шагам», и это нормально: так проще думать. Но иногда шагов становится слишком много, и линтер аккуратно намекает: «кажется, можно проще».
package todo
func Add(a, b int) int {
sum := a + b
return sum
}
Это не ошибка. Но во многих проектах такой стиль будут упрощать, потому что он добавляет шум. Более прямой вариант:
package todo
func Add(a, b int) int {
return a + b
}
Тут важная оговорка: если переменная sum нужна для объяснения смысла (например, в учебном коде), то иногда оставить её — нормально. Линтер не запрещает «лишнюю» переменную. Он подсказывает: «это можно убрать».
4. golangci-lint: запуск множества линтеров
Когда вы впервые слышите про golangci-lint, звучит как что-то из мира сельхозтехники: «вышел в поле, собрал урожай предупреждений». По сути так и есть: это агрегатор, который умеет запускать много разных линтеров за один прогон и собирать результаты в единый формат.
Почему это удобно? Потому что без агрегатора у вас начинается зоопарк: один линтер так запускается, другой иначе, третий выдаёт сообщения в своём стиле. А ещё хочется, чтобы вся команда видела одинаковые результаты.
При этом golangci-lint опасен для новичка именно удобством: очень легко «включить всё» и получить сотни сообщений, из которых половина про стиль, четверть про вкусовщину, и только маленькая часть — про реальные проблемы. В этот момент мозг говорит: «ну его», и линтеры выключаются навсегда. Поэтому цель — научиться включать их дозированно, чтобы сигналы оставались полезными.
Хорошая новость: golangci-lint как раз и создан так, чтобы вы могли выбрать набор линтеров, настроить исключения и жить спокойно, а не в режиме вечного пожара.
5. Как не утонуть в предупреждениях
Почти все проблемы с линтерами начинаются не из-за самих линтеров, а из-за неправильной тактики. Человек видит 300 предупреждений и пытается героически исправить всё сразу — а потом ненавидит и линтер, и код, и человечество.
Рабочая стратегия начинается с простого принципа: предупреждения бывают разной «силы». Какие-то похожи на баг и почти всегда требуют правки. Какие-то — полезный совет, но не обязаны быть выполнены любой ценой. Удобно (даже если вы не помните конкретные коды проверок) делить сигналы по смыслу.
| «Сила» сигнала | Как воспринимать | Пример по смыслу |
|---|---|---|
| Высокая | «остановись, проверь логику» | бессмысленное присваивание, мёртвый код, подозрительная проверка |
| Средняя | «скорее всего стоит упростить» | лишние ветвления, очевидные упрощения |
| Низкая | «совет по стилю» | косметика, формат сообщений, спорные предпочтения |
Дальше включается правило здравого смысла: если предупреждение высокой силы — мы почти всегда чиним. Если средней — чиним, если реально улучшает читаемость и не ломает намерение. Если низкой — выбираем: либо принимаем правило как стандарт проекта, либо отключаем его, чтобы не создавать шум.
Ещё одна важная привычка: не «замалчивать» предупреждения, пока не поняли, о чём они. Если вы не можете объяснить вслух, что означало предупреждение и почему вы его игнорируете, то, скорее всего, вы игнорируете потенциальный баг.
Минимальная настройка golangci-lint
Настройка линтера — тонкая штука: хочется «идеально», а получается «никто не запускает». Поэтому новичкам лучше начинать с короткого конфига: включить несколько реально полезных линтеров и не устраивать конкурс на самый строгий стиль.
Пример минимального .golangci.yml, который обычно даёт хороший баланс:
linters:
enable:
- staticcheck
- govet
- errcheck
run:
timeout: 2m
Это не «единственно правильный» вариант. Это идея: вы включаете небольшой набор линтеров, которые чаще ловят реальные проблемы. А дальше уже смотрите на опыт: слишком шумно — убавляете, мало полезных находок — добавляете точечно.
Если какой-то линтер постоянно спорит с вашим стилем и мешает читать код, его лучше отключить, чем «побеждать» его в каждой строке. Цель — читаемый и корректный код, а не «нулевое количество предупреждений любой ценой».
Когда допустимо игнорировать предупреждение
Иногда предупреждение нужно проигнорировать. Не потому что «лень», а потому что конкретно в этом месте вы сознательно выбрали такой код. Например, вы оставили более подробный вариант ради обучающей ясности, или вам действительно нужно нестандартное поведение.
Но игнорирование должно быть осмысленным и локальным. В Go-экосистеме распространён подход: если вы отключаете предупреждение, вы делаете это максимально точечно и желательно с коротким пояснением.
Часто это выглядит примерно так (пример не привязан к конкретному линтеру, важна идея):
package todo
import "fmt"
func debugTask(title string) {
//nolint:forbidigo // в учебном примере сознательно печатаем отладку
fmt.Println("debug:", title) // debug: buy milk
}
Смысл такой: вы не выключаете линтер навсегда, вы говорите «вот здесь я понимаю, что делаю». Если у вас //nolint расползается по проекту как плесень — это сигнал, что правила выбраны плохо или что код пора переписать проще.
6. Типичные ошибки при работе с линтерами
Ошибка №1: включить все линтеры сразу и получить «снежную лавину».
Когда предупреждений сотни, они перестают быть сигналами и превращаются в фон. Новичок быстро привыкает игнорировать всё подряд, а это убивает смысл инструмента. Гораздо эффективнее начать с малого набора и расширять только тогда, когда вы реально понимаете, зачем вам новое правило.
Ошибка №2: исправлять предупреждение, не понимая его смысла.
Это опаснее, чем кажется. Можно «поправить» код так, что линтер замолчит, но логика станет хуже или появится скрытый баг. Правильная реакция на предупреждение — сначала понять, что именно подозрительно, и только потом менять код. Если понимание не приходит, лучше остановиться и разобрать пример вручную.
Ошибка №3: делать код менее читаемым ради «зелёного отчёта».
Иногда линтер предлагает упрощение, но в вашем контексте оно делает код менее понятным, особенно в учебных примерах. Это нормально. Линтер — советчик, а не закон. Если правка ухудшает ясность, лучше оставить как есть или ослабить правило, чем превратить код в «победу над линтером» ценой читателя.
Ошибка №4: воспринимать линтер как замену тестов или как истину в последней инстанции.
Линтеры — это статический анализ: он очень силён в поиске некоторых классов проблем, но не проверяет поведение программы на сценариях. А ещё линтеры могут ошибаться или не знать ваших бизнес-правил. Поэтому правильная позиция спокойная: линтеры дополняют компилятор и go vet, но не заменяют ни мышление, ни проверки поведения.
Ошибка №5: замалчивать предупреждения массово, вместо того чтобы настроить правила.
Если проект весь в //nolint, это значит, что вы боретесь с инструментом, а не используете его. Обычно лучше сделать наоборот: настроить список линтеров и правил так, чтобы подавляющее большинство предупреждений были действительно значимыми. Тогда редкие //nolint будут выглядеть как осознанное исключение, а не как стиль жизни.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ