Опирается на правила:
GO-LINT-1…GO-LINT-6,GO-LINT-X1из Go Style Guide → раздел 11. Enforcement через golangci-lint.
Важно знать
.golangci.ymlобязателен в корне каждого Go-сервиса — без него правила существуют только на бумаге.- Минимальный набор линтеров:
errcheck,errorlint,gocritic,revive,gosimple,staticcheck,unused,lll.errcheckловит проглоченные ошибки — единственное исключение_ = f()с объяснением причины.errorlintзапрещаетerr == ErrXи прямой каст безerrors.As— использоватьerrors.Is/errors.As.staticcheckпроверяет семантику: устаревшие API, неиспользуемые результаты; группыSA,S,ST— все включены.//nolint:<linter>без комментария «зачем» — fail в CI.- Глобальное
nolint:allили отключениеerrcheckбез архитектурного решения — запрещено.
Форматирование gofmt убирает споры о пробелах. golangci-lint закрывает следующий класс проблем: проглоченные ошибки, нарушенные инварианты errors.Is/errors.As, стилистику, неиспользуемые символы, устаревшие API. Без него команда договаривается «на словах» — договорённость живёт до первого PR с _ = repo.Save(ctx, order).
.golangci.yml — базовый конфиг
GO-LINT-1: файл конфигурации в корне каждого сервиса.
run:
timeout: 5m
linters:
enable:
- errcheck
- errorlint
- gocritic
- revive
- gosimple
- staticcheck
- unused
- lll
linters-settings:
lll:
line-length: 120
errcheck:
check-type-assertions: true
check-blank: true
staticcheck:
checks:
- "SA*"
- "S*"
- "ST*"
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
CI-шаг в .github/workflows/ci.yml:
- name: lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.59.1
args: --timeout=5m
Версия линтера фиксируется явно — latest может сломать сборку при обновлении правил без предупреждения.
errcheck — проглоченные ошибки
GO-LINT-2: errcheck ловит случаи, когда возвращённая ошибка игнорируется без объяснения.
Типичная ситуация в коде OrderRepository:
func (r *repository) Save(ctx context.Context, order *Order) error {
_, err := r.db.Exec(ctx, insertOrderSQL, order.ID, order.CustomerID, order.Total)
return err
}
func (s *service) CreateOrder(ctx context.Context, cmd CreateOrderCommand) error {
order := NewOrder(cmd.CustomerID, cmd.Items)
s.repo.Save(ctx, order) // errcheck: возвращаемая ошибка проглочена
return nil
}
Правило допускает единственное исключение — явный _ = с объяснением в комментарии godoc или в том же выражении; в коде без пояснений:
// Аккумулируем ошибки закрытия всех ресурсов, основной поток возвращает первую.
_ = rows.Close()
При check-blank: true даже _ = f() попадает в lint-вывод; если исключение оправдано, добавляется //nolint:errcheck с обоснованием (см. раздел про nolint ниже).
errorlint — errors.Is и errors.As
GO-LINT-3: прямое сравнение err == ErrX и приведение типа без errors.As нарушают цепочку оборачивания %w.
var ErrOrderNotFound = errors.New("order not found")
if err == ErrOrderNotFound {
return nil, apperr.NotFound("order", id)
}
if e, ok := err.(*InsufficientFundsError); ok {
return nil, e
}
После исправления:
if errors.Is(err, ErrOrderNotFound) {
return nil, apperr.NotFound("order", id)
}
var fundErr *InsufficientFundsError
if errors.As(err, &fundErr) {
return nil, fundErr
}
errorlint срабатывает на оба антипаттерна автоматически — ручная проверка в ревью не нужна.
gocritic и revive — стилистика и идиомы
GO-LINT-4: два линтера покрывают разные классы стилистических нарушений.
gocritic ловит неидиоматичные конструкции:
// gocritic: appendAssign — переменная перезаписывает себя через append
items = append(items, items...)
// gocritic: sloppyLen — len(s) >= 1 следует заменить на len(s) > 0
if len(order.Items) >= 1 {
revive — линтер на основе golint, покрывает конвенции именования и контракты публичного API:
// revive: exported — экспортируемая функция без godoc-комментария
func ProcessOrder(ctx context.Context, cmd ProcessOrderCommand) error {
// revive: error-return — error должна быть последним возвращаемым значением
func GetCustomer(ctx context.Context, id string) (error, *Customer) {
//nolint:gocritic и //nolint:revive допустимы при осознанном отклонении — с обоснованием в комментарии рядом.
staticcheck — семантика и устаревшие API
GO-LINT-5: staticcheck проверяет то, что go vet и форматтер не видят.
Три группы правил, включённых в конфиге:
SA*(Static Analysis) — реальные баги: разыменование nil, неправильныйsync.Mutex, неиспользуемые параметры вfmt.Sprintf.S*(Simplifications) — упрощения:x = x + 1→x++,strings.IndexRuneвместоstrings.Indexдляrune.ST*(Stylecheck) — стиль по Effective Go: правила именования, doc-комментарии.
Пример диагностики SA1006 — динамический первый аргумент Printf (потенциальная уязвимость форматной строки):
msg := "order " + id + " processed"
log.Printf(msg) // SA1006: динамическая строка как первый аргумент Printf
log.Printf("order %s processed", id) // правильно
И S1039 — избыточный Sprintf:
log.Printf(fmt.Sprintf("order %s processed", id)) // S1039: вложенный Sprintf не нужен
log.Printf("order %s processed", id) // правильно
Группа SA* выявляет реальные баги в коде CustomerService, ProductRepository и другой доменной логике до запуска тестов.
nolint-политика
GO-LINT-6: //nolint:<linter> без объяснения ломает CI.
Принятая форма:
//nolint:errcheck // rows.Close() в defer: ошибка закрытия уже залогирована выше
defer rows.Close()
//nolint:gocritic // appendAssign намеренно для дедупликации на месте без аллокации
items = append(items[:i], items[i+1:]...)
Форма, которую CI отклоняет:
//nolint:errcheck
defer rows.Close()
//nolint:all
func (r *repository) BulkInsert(...) error {
Bare //nolint без имени конкретного линтера отключает всё — CI-шаг с grep -r '//nolint$\|//nolint:all' превращает это в build failure.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Отсутствие .golangci.yml в корне сервиса | GO-LINT-1 | Добавить конфиг с минимальным набором линтеров |
s.repo.Save(ctx, order) без проверки ошибки | GO-LINT-2 | if err := s.repo.Save(ctx, order); err != nil { ... } |
err == ErrOrderNotFound | GO-LINT-3 | errors.Is(err, ErrOrderNotFound) |
err.(*InsufficientFundsError) без errors.As | GO-LINT-3 | errors.As(err, &fundErr) |
//nolint:errcheck без комментария «зачем» | GO-LINT-6 | //nolint:errcheck // причина |
//nolint:all | GO-LINT-X1 | Исправить нарушение или точечный //nolint:<linter> с обоснованием |
Отключение errcheck глобально в конфиге | GO-LINT-X1 | Разрешить конкретные исключения через exclude-rules |
staticcheck без групп SA, S, ST | GO-LINT-5 | Включить все три группы в checks |
Куда дальше
- Форматирование —
gofmt,goimports, длина строки 100–120; фундамент, поверх которого работаетlll. - Типы и интерфейсы —
anyбез типизации и embedding; часть нарушений ловитgocritic. - Управляющие структуры — guard clause и структура функций;
reviveсигнализирует о глубокой вложенности. - Раздел Security → SAST —
gosecиgolangci-lintв связке: SQLi, G404, SARIF в Security tab.