Автоматизация оптимизаций в Go

image

Привет, Хабр! Меня зовут Денис Лимарев, я разработчик платежной системы в Delivery Club.
Недавно мы провели два митапа: по оптимизациям и по нашему новому линтеру. На первом митапе разобрали оптимизации кода на Go, а в рамках второго поговорили про создание и возможности нашего нового линтера, который может искать и самостоятельно применять эти оптимизации, и не только. Как делался линтер и поиск каких оптимизаций смогли автоматизировать — читайте под катом.

Как устроен линтер


Линтер реализован на базе библиотеки go-ruleguard (далее ruleguard) в виде набора правил. Библиотека разработана Искандером Шариповым и контрибьютерами. В свою очередь, ruleguard для анализа кода на Go использует gogrep, разработанную Даниэлем Мартином и единомышленниками. В последних версиях ruleguard стала использовать модифицированную gogrep, так как Даниэль прекратил поддерживать свою библиотеку.

Архитектура ruleguard рассчитана на лёгкое и быстрое создание правил для статического анализа, подробнее про это можно почитать в статье Искандера. Так, например, реализация правила через ruleguard для проверки defer в цикле у меня заняла около часа, а вариант с обходом ast — около 3–4 часов, не считая тестов. Именно эта простота и лёгкость использования привлекают к созданию всё новых и новых правил на базе ruleguard. Это помогает увеличивать покрытие кода проверками: оптимизаций, стиля кода, возможных упрощений, опечаток и так далее.

Есть три способа запуска линтера: через ruleguard, gocritic и golangci-lint (далее golangci). Это возможно благодаря тому, что ruleguard интегрирован с gocritic, а та интегрирована с golangci. В результате получаем возможность бесшовно встраивать новый линтер в существующие этапы сборки в CI/CD при условии использования в нём golangci/gocritic.

Так что там всё-таки про оптимизации?


Оптимизации, которые были рассмотрены в рамках первого митапа и после реализованы в линтере в рамках второго:

  1. defer в цикле
    Суть этого правила в уменьшении нагрузки на стек и предотвращении возможных ошибок по утечкам ресурсов. При обнаружении defer в цикле линтер предупредит о вероятной утечке. Автоматически заменять код в данном случае не получится, так как есть очень большая вариативность возможных решений и алгоритмически предсказать оптимальный вариант очень сложно.
  2. Отправка запросов в БД без контекста
    Суть оптимизации в ограничении максимальной длительности ответа от БД путём отправки запроса вместе с контекстом. При обнаружении использования метода, не предполагающего передачу контекста, линтер выдаст предупреждение. На данный момент явно поддерживаются методы стандартной SQL-библиотеки и jmoiron/sqlx. Автозамена кода в таком случае не предусмотрена, так как новая сигнатура метода будет отличаться от предыдущей.
  3. Компиляция регулярных выражений
    На данный момент правило смотрит исключительно компиляцию регулярных выражений, использующих постоянные значения, в цикле:
    for { ok := regexp.MatchString("foo", "bar foo") ; /.../ }
    

    Регулярные выражения не изменяют своего состояния между вызовами, поэтому их достаточно один раз скомпилировать и переиспользовать. Автозамена кода в этом случае не предусмотрена.
  4. Оптимизации в циклах
    • Итерация по индексу слайсов при большом размере элементов для избежания их копирования.
    • Итерация по указателю массива при большом размере массива. Подробнее можно почитать тут.

Помимо описанных правил линтер также проверяет:

  1. Стиль кода по Go-правилам Delivery Club: правила именования методов и пакетов.
  2. Упрощения кода: проверка ошибок, неиспользуемое форматирование.
  3. Возможные ошибки логики: непроверяемые приведения типов, defer в цикле, не закрытые ресурсы.


Как подключить линтер


Напомню, что линтер имеет три варианта подключения к проекту: golangci, gocritic, ruleguard. Рассмотрим подключение через golangci. Для него необходимо будет добавить в проект файл, экспортирующий необходимые наборы правил:

//go:build ignore
// +build ignore
 
func init() {
    dsl.ImportRules("", rules.Bundle)
    dsl.ImportRules("", dcRules.Bundle)
}


после чего добавим пару строк в конфигурацию golangci:

linters:
  enable:
    - gocritic
linters-settings:
  gocritic:
    enabled-checks:
      - ruleguard
    settings:
      ruleguard:
        rules: "linter.go"


Пример подключения линтера к проекту можно посмотреть тут. По окончании настройки, для запуска линтера достаточно выполнить следующую команду:

golangci-lint run --config=.golangci.yml ./...


Какие ещё есть возможности


Помимо основного набора правил архитектура ruleguard позволяет добавлять проверки, характерные исключительно для определённого проекта или команды разработчиков. Поэтому можно расширять уже существующий набор правил DC локальными практиками и уточнениями. Например, для проектов, тесно взаимодействующих с временными зонами, возможна проверка приведения к UTC.

func timeUtc(m dsl.Matcher) {
    m.Match(`time.Now().$method`).
        Where(!m["method"].Text.Matches(`UTC\(\)`)).
        Report("maybe UTC() call was forgotten").
        Suggest("time.Now().UTC().$method").
        At(m["method"])
}


Линтер умеет автоматически корректировать код при обнаружении необходимого паттерна. Сейчас поддерживаются только однострочные правки. Код для автозамены описывается в методе Suggest, а в методе At указывается участок, который будет заменён. Автозамена включается только при передаче флага --fix в golangci/ruleguard.

Также среди возможностей: версионирование правил, полный обход AST при условии подключения проверок в виде Go-плагинов.

Планы по развитию линтера


  1. Добавление возможности игнорирования конкретных правил в конфиге — реализовано в gocritic в рамках github.com/go-critic/go-critic/issues/1176. В golangci поддержка ожидается в ближайшем релизе.
  2. Поддержка возможности быстрого исправления кода на golangci — реализовано в рамках github.com/go-critic/go-critic/issues/1179. В golangci поддержка ожидается в ближайшем релизе.
  3. Помощь в реализации и тестировании функциональности sub-match в ruleguard.
  4. Компиляция линтера в виде отдельного исполняемого файла.
  5. Добавление новых правил.


Список полезных ссылок


  1. Репозиторий линтера
  2. Репозиторий ruleguard
  3. Пример установки линтера в проекте
  4. Go-ruleguard: динамические проверки для Go
  5. Go-ruleguard: подключение нескольких наборов правил
  6. Гайд по написанию проверок ruleguard
  7. DSL-документация
  8. Go-perfguard: набор правил для повышения производительности
  9. Примеры правил в Grafana
  10. Примеры правил в Uber

© Habrahabr.ru