Опирается на правила: GO-LINT-1GO-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 + 1x++, 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-2if err := s.repo.Save(ctx, order); err != nil { ... }
err == ErrOrderNotFoundGO-LINT-3errors.Is(err, ErrOrderNotFound)
err.(*InsufficientFundsError) без errors.AsGO-LINT-3errors.As(err, &fundErr)
//nolint:errcheck без комментария «зачем»GO-LINT-6//nolint:errcheck // причина
//nolint:allGO-LINT-X1Исправить нарушение или точечный //nolint:<linter> с обоснованием
Отключение errcheck глобально в конфигеGO-LINT-X1Разрешить конкретные исключения через exclude-rules
staticcheck без групп SA, S, STGO-LINT-5Включить все три группы в checks

Куда дальше

  • Форматирование — gofmt, goimports, длина строки 100–120; фундамент, поверх которого работает lll.
  • Типы и интерфейсы — any без типизации и embedding; часть нарушений ловит gocritic.
  • Управляющие структуры — guard clause и структура функций; revive сигнализирует о глубокой вложенности.
  • Раздел Security → SAST — gosec и golangci-lint в связке: SQLi, G404, SARIF в Security tab.