При разработке на Go активно используются линтеры — программы для статического анализа кода. Металинтер golangci-lint позволяет запустить десятки линтеров одновременно, чтобы сократить время выполнения до нескольких секунд. Мы поговорили с создателем golangci-lint Денисом Исаевым — среди российских разработчиков он внес, возможно, самый большой вклад в экосистему Go (Golang). Денис рассказал, как родилась идея металинтера, и как golangci-lint за несколько лет превратился из личного pet-проекта в стандарт, которым пользуются тысячи команд по всему миру.
Денис, как вам пришла идея создать golangci-lint, и пересекался ли этот проект с вашими рабочими задачами на тот момент?
В
Цикл разработки устроен так, что сначала мы пишем код, а затем запускаем автоматические тесты, которые проверяют его корректность. Чтобы ускорить процесс проверки, после автотестов мы используем линтеры — программы-анализаторы, которые выявляют потенциальные ошибки, стилистические нарушения и другие недочеты.
На тот момент существовало около 20 линтеров, и они позволяли покрывать значительный класс ошибок в Go. Но их работа занимала на нашем проекте около 3 минут — это довольно значительное время, если необходимо провести несколько проверок за день. Уже тогда я начал думать над тем, как сократить это процесс — например, создать металинтер, который мог бы параллельно запускать все проверки.
Когда в 2018 году я устроился в Яндекс, у меня была пара свободных недель перед выходом на работу. И я решил, что успею реализовать свою идею. На тот момент в Яндексе не был распространен язык Go, и я создал golangci-lint просто из интереса, без какой-либо коммерческой выгоды.
Какие проблемы решал ваш металинтер в момент запуска, и изменилось ли это со временем?
Главной задачей было превратить 3 минуты проверки кода в 10 секунд. Так и получилось — а с развитием проекта скорость только нарастала. Через год после запуска металинтера он работал уже в 100 раз быстрее, чем исходное решение, а через пару лет удалось ускорить его еще в
Заслуга здесь не только моей идеи и команды open-source контрибьютеров, но и специалистов из core-команды самого языка Go. Увидев, с какими проблемами сталкиваются разработчики линтеров, они выпустили набор библиотек, стандартов и рекомендаций, с помощью которых можно было значительно ускорить работу.
Как устроена архитектура golangci-lint? Что позволило сделать его таким быстрым?
Я уже был знаком с проектом gometalinter, который запускает 20 линтеров параллельно. Я погрузился в его работу и понял, что примерно 90% действий каждого линтера дублируется: прочитать исходный код проекта, распарсить его, прогнать по нему базовый анализ и передать в логику конкретного линтера. И у меня появилась идея: сделать так, чтобы эти 90% работы делать только один раз.
Сложность заключалась в том, что через интерфейсы линтеров нельзя было настроить отдельные этапы работ — например, пропустить уже сделанные предыдущими линтерами шаги. Поэтому моя основная работа состояла в создании новых интерфейсов.
Я вручную клонировал каждый из линтеров и изменил их исходный код, чтобы добавить интерфейс, который позволил бы работать только с отдельными этапами. И далее в своем golangci-lint я уже просто вызывал этот интерфейс. С помощью этих шагов я получил x10 к скорости.
Это было специфическое и довольно рискованное решение. На старте оно даже вызвало отторжение в сообществе. Чтобы объединить линтеры в свой golangci-lint мне пришлось клонировать каждый из них, и у других разработчиков появились опасения, что основная версия будет развиваться отдельно, а мой форк останется позади. При этом в каждый линтер я отправил pull request, чтобы разработчики забрали мои изменения к себе — но на практике половина из них не отреагировали.
Всё это вызвало критику в сообществе, так как мой подход был нестандартным. Обычно принято сначала вливать изменения в основную версию, а затем использовать их в основной версии — а у меня получилось наоборот. Но я понимал, что если ждать реакции других авторов, которые могут быть заняты своими делами, то реализация займет годы. Поэтому я решил рискнуть — и не прогадал.
Ваш проект стал по сути стандартом статического анализа кода в Go. Как вы думаете, в чем его главное преимущество перед другими линтерами, и почему именно он так широко распространился?
Ключевой особенностью проекта стала скорость — ни одно решение не позволяло так упростить и ускорить тестирование кода. Также golangci-lint очень приятен в использовании — своеобразный iPhone в мире open source. Немногие продукты с открытым кодом в то время могли таким похвастаться.
Чтобы достичь такого эффекта, я использовал более удобный формат конфигурации. В прошлом решении — gometalinter — применялся JSON-формат. Он более «машиночитаемый», но просматривать и редактировать его не так комфортно. Я перешел в своем решении на YAML, с которым человеку работать гораздо удобнее.
Также я написал подробную документацию и вложился в маркетинговый текст, где упомянул возможности, преимущества и недостатки моего решения по сравнению с конкурентами. Еще одним новшеством для мира open source стало демо — видеозапись, где я показал, как работает мой продукт. Сейчас многие разработчики тоже размещают демо на репозитории, но тогда это было редкостью.
В развитии продукта мне помогла обратная связь. Например, golangci-lint внедрили в работу мои бывшие коллеги из Mail.Ru Group. Они рассказали, что металинтер крайне сложно использовать в крупном проекте, который существует уже несколько лет. Если запустить линтер на всю кодовую базу, он покажет миллионы ошибок, накопленных за эти годы — и их исправление займет колоссальное количество времени.
Я придумал, как с этим справиться — нужно было научить металинтер запускать проверку кода только для внесенных изменений. Например, разработчик пишет код, меняя 1% кодовой базы — и только эту часть проверяет линтером. Мое усовершенствование помогло сделать golangci-lint еще более универсальным — чтобы его можно было применять на проектах любого объема.
Как golangci-lint повлиял на экосистему Go?
Прежде всего, мне хочется отметить, как он повлиял на саму команду разработки языка Go в Google — мы пообщались с ними на конференции GopherCon в 2019 году. Увидев golangci-lint, разработчики Go увидели проблему использования линтеров, с которой мне пришлось бороться — так появился стандарт интерфейса, который теперь используется во всех линтерах. Таким образом, я поспособстовал тому, чтобы облегчить работу всем разработчикам Go.
С 2019 года golangci-lint стал использоваться в большинстве новых проектов на Go — и это привело к двум следствиям. Во-первых, линтеров стало значительно больше — по умолчанию через golangci-lint запускается около
Во-вторых, я показал, как быстро делать новые линтеры, написав туториал по их разработке и встраиванию в golangci-lint. С тех пор энтузиасты со всего мира создали десятки или даже сотни линтеров, развивая наше сообщество. Так у разработчиков появился широкий выбор — какие именно решения использовать для своего проекта на Go.
Когда вы начинали этот проект, то предполагали, что он будет развиваться таким образом? И что в нем происходит сейчас?
Было ожидание, что я решу проблему, которая заботит не только меня, но и коллег — и в целом сделаю интересный open-source продукт. Но я не думал, что он станет таким популярным, и его будут использовать почти все разработчики на Go.
Чтобы продукт развивался дальше, я принял решение открыть сообщество контрибьютеров и сделать проект децентрализованным. Я запустил бота, который автоматически выдает права контрибьютера человеку, который внес хотя бы одно изменение в код проекта. Такая открытость снова вызвала скепсис в сообществе, но и тут моя ставка выстрелила — появилось несколько активных участников, которые значительно поддерживают проект.
Это освободило мне время для других важных проектов, но при этом я слежу за развитием golangci-lint и помогаю другим разработчикам. Сейчас среди участников проекта уже около тысячи специалистов, и сотни из них внесли важные усовершенствования.
Если бы вы запускали golangci-lint сегодня, что бы вы сделали иначе? Есть ли у вас уже идеи для следующего большого проекта — в области автоматизации кода или чего-то еще?
Мой подход к golangci-lint был чисто инженерным — проигнорировать то, как в сообществе принято работать со статическим анализом, и просто решать проблему. Первые два года такой подход работал, но позже я понял, что на этом далеко не уедешь. Проблема давно исследована, есть сотни научных статей на эту тему — и нужно изучить базовые понятия, чтобы работать не только над скоростью, но и над качеством анализа.
Так я стал использовать более сложные алгоритмы — например, пару месяцев переписывал алгоритм обхода графа всех исходных файлов, чтобы он был более эффективным. На самом деле, можно было начать это раньше — и тогда проект развивался бы на основе более фундаментальных вещей.
Сейчас у меня есть идея радикального повышения качества анализа — метрик точности и полноты. Точность определяет, сколько найденных проблем являются действительно критичными, а полнота — какое количество недочетов из всех имеющихся найдет наш металинтер.
На данный момент во всех линтерах не самая высокая точность и полнота — фундаментальные решения в других языках более совершенны. Но чтобы улучшить линтеры, нужно применять другие подходы для их разработки — и мне было бы интересно над этим подумать.