Если ты работаешь с Go, то рано или поздно сталкиваешься с вопросом: как гарантировать качество кода, когда в команде десять разработчиков, каждый со своим стилем? Линтеры — это ответ. Но не просто "инструмент, который ругается на запятые". Это целая стратегия поиска реальных проблем до того, как код попадёт в продакшн.
В этой статье разберу, как работают линтеры в Go, какие инструменты использовать, как их настроить и интегрировать в CI/CD. И да, я буду честен — не все инструменты одинаково полезны.
Что вообще делают линтеры и почему они важны
Начнём издалека. Код — это не просто текст, который компилируется. Это договор между тобой и твоей командой о том, как всё будет устроено. Компилятор проверяет только синтаксис и типы. Линтеры проверяют остальное.
По-хорошему, здесь нужно различить два подхода:
Статический анализ — это инструменты, которые читают исходный код без его запуска и ищут потенциальные проблемы. Баги, уязвимости, неэффективный код, нарушения стиля. Всё это можно поймать, не запуская приложение.
Динамический анализ — это когда код уже работает. Профайлеры, race detectors, мониторинг утечек памяти. В Go это go test -race, pprof и подобное.
Линтеры — это инструменты статического анализа. И если честно, большинство команд используют их недостаточно эффективно. Просто запустили golangci-lint, увидели 500 ошибок, выключили половину правил и забыли. Неправильный подход.
golangci-lint: король линтеров в Go
На одном проекте я встретился с ситуацией, когда в репозитории было четыре разных линтера, работающих отдельно. Каждый вызывался вручную перед коммитом. Разработчики забывали, конфликты в правилах, chaos. Потом мы перешли на golangci-lint — и это изменило всё.
golangci-lint — это не один линтер. Это агрегатор, который запускает под капотом десятки специализированных инструментов и выдаёт единый отчёт. Вот что там работает:
golint/revive — проверяет стиль кода и соглашения Go. Имена переменных, экспортируемые функции должны иметь документацию и т.д.
go vet — встроенный в Go инструмент, ловит явные ошибки: неправильное использование fmt, потенциальные паники, неправильные тесты.
errcheck — находит необработанные ошибки. Это мощный инструмент, потому что в Go ошибки возвращаются явно, и их легко случайно проигнорировать.
goimports — форматирует импорты и удаляет неиспользуемые. Мелочь, но экономит время при code review.
gosec — поиск проблем безопасности. SQL-инъекции, использование слабых криптографических функций, небезопасные операции с файлами.
golangci-lint имеет встроенные пресеты. Например, default включает самые важные инструменты, а ты всегда можешь настроить свой набор.
Вот как выглядит базовая конфигурация:
# .golangci.yml
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- gosec
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
Установка простая:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run ./...
А вот запуск из CI:
golangci-lint run --deadline=5m --out-format json > lint-report.json
Другие линтеры, которые стоит знать
golangci-lint это мощно, но иногда нужны специализированные инструменты.
staticcheck — по сути, это go vet на стероидах. Находит недостижимый код, неправильные регулярные выражения, логические ошибки. Вообще, если golangci-lint когда-нибудь отключат, я буду использовать именно staticcheck.
go-critic — ловит код, который технически правильный, но странный. Например, условие, которое всегда true, или параметры функции, которые не используются. Это не ошибки, но это красный флаг.
unconvert — находит ненужные преобразования типов. Кажется мелочью, но в больших проектах такие мелочи накапливаются.
На одном проекте я добавил misspell — проверяет опечатки в строках и комментариях. Звучит странно, но когда пользователь видит "Authentification failed" вместо "Authentication failed" — это выглядит непрофессионально.
Вот как включить их все в golangci-lint:
linters:
enable-all: true
disable:
- depguard # слишком строг
- exhaustivestruct # раздражает
- maligned # deprecated
Честно? enable-all — это опасная штука. Включишь всё, получишь тысячу ошибок. Лучше начни с малого и добавляй постепенно.
Интеграция в CI/CD: как это работает на практике
Линтеры в локальной среде — это хорошо. Но настоящая мощь проявляется в CI/CD, когда каждый pull request проверяется автоматически.
Вот как мы это делаем в Distiq для своих проектов на Go:
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=5m
Для GitLab:
# .gitlab-ci.yml
lint:
stage: test
image: golangci/golangci-lint:latest
script:
- golangci-lint run --deadline 5m --out-format json > report.json
artifacts:
reports:
codequality: report.json
Важный момент: линтеры должны работать быстро. Если разработчик ждёт 10 минут результата CI, он будет хитрить и пушить в обход проверок. Поэтому я выставляю timeout в 5 минут и убираю лишние правила из critical path.
Как настроить линтеры под свой проект
Вот здесь начинается реальная работа. На одном проекте я видел конфиг golangci-lint на 200 строк. Половину можно было выкинуть.
Правило номер один: не копируй конфиги с других проектов. Каждая команда имеет свой стиль. Для одного проекта важна максимальная безопасность (финтех), для другого — скорость разработки (стартап). Это влияет на то, какие правила включать.
Вот мой approach:
Шаг 1: начни с минимума
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
Это пять самых важных линтеров. Они находят реальные баги.
Шаг 2: добавь безопасность, если нужна
Для приложений, где важна безопасность, включи gosec и ищи потенциальные уязвимости.
Шаг 3: настрой исключения
Не все правила работают для всех случаев:
issues:
exclude-rules:
# Тесты часто нарушают правила стиля
- path: _test\.go
linters:
- gosec
- govet
# Генерированный код не нужно проверять
- path: zz_generated
linters:
- '*'
# Некоторые ошибки можно игнорировать в определённых пакетах
- path: internal/legacy
linters:
- errcheck
text: "Error return value not checked"
Шаг 4: не зацикливайся на стиле
Если у тебя уже есть gofmt, то revive становится лишним. Выбери один инструмент форматирования и стоп.
На одном проекте я видел, как разработчики тратили часы на споры о форматировании кода. А потом мы просто включили gofmt, и проблема испарилась. Инструмент решил то, что люди не могли решить.
Локальная разработка: pre-commit hooks
Если линтеры работают только в CI, то feedback loop долгий. Разработчик пушит, ждёт результата, исправляет. Неэффективно.
Лучше запускать линтеры локально перед коммитом.
# .git/hooks/pre-commit
#!/bin/bash
golangci-lint run
if [ $? -ne 0 ]; then
echo "Lint failed. Commit aborted."
exit 1
fi
Или используй готовые инструменты. Например, pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v1.54.2
hooks:
- id: golangci-lint
Потом просто:
pre-commit install
pre-commit run --all-files
Теперь перед каждым коммитом линтер запустится автоматически. Никто не забудет.
Сравнение подходов: когда использовать что
Есть три основных сценария:
Зелёное поле (новый проект)
Здесь можно быть строгим. Включи максимум правил, установи высокие стандарты сразу. Потом расслабиться уже не получится.
Существующий проект с старым кодом
Не начинай с enable-all. Получишь тысячу ошибок, разработчики возненавидят линтеры. Вместо этого включи постепенно. Сначала самые важные (errcheck, govet), потом остальные.
Я видел такой подход: добавить новое правило, создать отдельную GitHub issue, дать команде неделю на исправления, потом включить в CI. Работает.
Критичный проект (финтех, медицина)
Здесь нужна максимальная строгость. gossec обязателен, depguard для контроля зависимостей, ограничения на использование unsafe и конкурентности.
linters:
enable:
- gosec
- depguard
- govet
- errcheck
- staticcheck
- unused
- ineffassign
linters-settings:
depguard:
rules:
main:
deny:
- name: io/ioutil
reason: "Use io or os packages instead"
Когда линтеры становятся вредными
Честно? Если линтер выключают половину разработчиков, значит что-то не так.
Я видел случаи, когда ложные срабатывания линтеров были настолько частыми, что люди перестали им доверять. Линтер ругается на всё подряд, включая совершенно корректный код — и вот уже никто не обращает внимания на его замечания.
Решение простое: регулярно пересматривай конфиг. Если правило срабатывает на 90% кода и каждый раз это false positive — удали его. Линтер должен помогать, а не мешать.
Я также видел проекты, где люди отключают errorcheck везде, потому что "в Go много ошибок". Это неправильно. Если ошибки действительно не нужно обрабатывать (что редко), то явно это закомментируй:
// Ошибку можно безопасно игнорировать, поскольку это вспомогательная логика
_ = someFunc()
// Или через blank identifier
if err != nil {
// Логируем, но не паникуем
log.Printf("Warning: %v", err)
}
Практический пример: конфиг для боевого проекта
Вот конфиг, который я бы использовал для production-проекта среднего размера:
# .golangci.yml
run:
timeout: 5m
deadline: 5m
linters:
enable:
- errcheck # Необработанные ошибки
- govet # Встроенные проверки
- gosimple # Упрощение кода
- staticcheck # Продвинутые проверки
- unused # Неиспользуемые переменные
- ineffassign # Неэффективные присваивания
- gosec # Безопасность
- goimports # Форматирование импортов
disable-all: false
fast: false
linters-settings:
gosec:
severity: high
confidence: medium
errcheck:
check-type-assertions: true
check-blank: true
output:
format: colored-line-number
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- govet
- path: cmd/
linters:
- gosec
text: "G304" #
