Привет, Хабр! Меня зовут Денис Лимарев, я разработчик платежной системы в 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.
Так что там всё-таки про оптимизации?
Оптимизации, которые были рассмотрены в рамках первого митапа и после реализованы в линтере в рамках второго:
-
defer в цикле
Суть этого правила в уменьшении нагрузки на стек и предотвращении возможных ошибок по утечкам ресурсов. При обнаружении defer в цикле линтер предупредит о вероятной утечке. Автоматически заменять код в данном случае не получится, так как есть очень большая вариативность возможных решений и алгоритмически предсказать оптимальный вариант очень сложно.
-
Отправка запросов в БД без контекста
Суть оптимизации в ограничении максимальной длительности ответа от БД путём отправки запроса вместе с контекстом. При обнаружении использования метода, не предполагающего передачу контекста, линтер выдаст предупреждение. На данный момент явно поддерживаются методы стандартной SQL-библиотеки и jmoiron/sqlx. Автозамена кода в таком случае не предусмотрена, так как новая сигнатура метода будет отличаться от предыдущей.
-
Компиляция регулярных выражений
На данный момент правило смотрит исключительно компиляцию регулярных выражений, использующих постоянные значения, в цикле:for { ok := regexp.MatchString("foo", "bar foo") ; /.../ }
Регулярные выражения не изменяют своего состояния между вызовами, поэтому их достаточно один раз скомпилировать и переиспользовать. Автозамена кода в этом случае не предусмотрена.
-
Оптимизации в циклах
- Итерация по индексу слайсов при большом размере элементов для избежания их копирования.
- Итерация по указателю массива при большом размере массива. Подробнее можно почитать тут.
- Итерация по индексу слайсов при большом размере элементов для избежания их копирования.
- Стиль кода по Go-правилам Delivery Club: правила именования методов и пакетов.
- Упрощения кода: проверка ошибок, неиспользуемое форматирование.
- Возможные ошибки логики: непроверяемые приведения типов, 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-плагинов.
Планы по развитию линтера
- Добавление возможности игнорирования конкретных правил в конфиге — реализовано в gocritic в рамках github.com/go-critic/go-critic/issues/1176. В golangci поддержка ожидается в ближайшем релизе.
- Поддержка возможности быстрого исправления кода на golangci — реализовано в рамках github.com/go-critic/go-critic/issues/1179. В golangci поддержка ожидается в ближайшем релизе.
- Помощь в реализации и тестировании функциональности sub-match в ruleguard.
- Компиляция линтера в виде отдельного исполняемого файла.
- Добавление новых правил.
Список полезных ссылок
- Репозиторий линтера
- Репозиторий ruleguard
- Пример установки линтера в проекте
- Go-ruleguard: динамические проверки для Go
- Go-ruleguard: подключение нескольких наборов правил
- Гайд по написанию проверок ruleguard
- DSL-документация
- Go-perfguard: набор правил для повышения производительности
- Примеры правил в Grafana
- Примеры правил в Uber
quasilyte
Рандомные советы, которые тоже можно было бы добавить в статью:
Кеш golangci-lint иногда приходится сбрасывать при использовании правил и их активном изменении. Есть даже issue об этом.
Как включать и отключать отдельные правила через golangci-lint конфиг, используя параметры enable/disable линтера gocritic/ruleguard.
Возможно, вы как-то решили проблему того, что в golangci-lint в yaml файла может быть неудобно прописывать путь к правилам? Есть, опять же, issue про это. Вроде как решилось тем, что теперь можно в конфиге для этого использовать интерполяцию. https://github.com/golangci/golangci-lint/pull/2308
Можно поделиться опытом тестирования диагностик. В go/analysis и ruleguard есть почти всё, чтобы это было удобным, но есть нюансы и продвинутые фичи. Например, можно ещё и quickfix'ы тестировать. :)
Описание всяких проблем в использований и путей для их обхода тоже было бы ценно. В том числе для меня, чтобы понять, какие вещи нужно фиксить в первую очередь внутри самого ruleguard.
В докладе по ссылке было побольше информации. Кажется, в статью можно было бы как минимум добавить недостающее и стало бы лучше.
Если планируется дорабатывать статью, то что-то из этого добавить всё ещё не поздно. Если же не планируется дорабатывать, то у читателей будут пара затравок на что посмотреть дальше.