JavaRush /Курсы /Go SELF /Линтеры в Go: staticcheck и golangci-lint

Линтеры в Go: staticcheck и golangci-lint

Go SELF
30 уровень , 3 лекция
Открыта

1. Введение

Если вы только начинаете, кажется логичным: «компилятор же умный, он всё скажет». Проблема в том, что компилятор обязан быть честным формалистом: если код корректен по правилам языка — он пропускает. Но корректный код может быть странным, хрупким или просто подозрительным. go vet добавляет официальный слой «похоже на ошибку», а линтеры расширяют эту идею: они находят больше паттернов, которые статистически часто оказываются багами или источниками боли в ревью.

Представьте, что компилятор — это охранник на входе в здание: «у вас пропуск есть? проходите». go vet — внимательный охранник, который ещё спрашивает: «а вы точно туда идёте?». А линтеры — коллега, который работает здесь давно и говорит: «слушай, ты можешь зайти, но вот здесь люди обычно падают в люк, будь осторожен». Иногда коллега ошибается, но чаще экономит вам часы.

Чтобы не путаться, полезно держать в голове разделение обязанностей:

Инструмент Что делает Что не делает
gofmt
приводит код к каноническому формату не чинит логику
goimports
формат + импорты «как надо» не лечит архитектуру зависимостей
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 будут выглядеть как осознанное исключение, а не как стиль жизни.

1
Задача
Go SELF, 30 уровень, 3 лекция
Недоступна
Готовность задачи
Готовность задачи
1
Задача
Go SELF, 30 уровень, 3 лекция
Недоступна
Следующий идентификатор
Следующий идентификатор
1
Задача
Go SELF, 30 уровень, 3 лекция
Недоступна
Два линтера
Два линтера
1
Задача
Go SELF, 30 уровень, 3 лекция
Недоступна
Ошибки записи
Ошибки записи
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ