Хороший разработчик обычно стремится написать код, который не просто работает, но работает стабильно. Код, который легко читать и сопровождать. Достичь такого высокого качества кода кроме тестов также помогают единый стиль кода, хорошая документация, а также простота и изящность, как на уровне всей системы, так и на уровне отдельных модулей и функций.
Считается, что соответствие кода принятым в команде стандартам качества проверяет как сам разработчик, так и его коллеги на код-ревью. Проблема только в том, что все мы люди, и всегда можем что-то упустить. А иногда (на самом деле очень часто), разработчики закрывают глаза на те моменты, которые кажутся им незначительными. В связи с этим придумали статический анализ кода. Если говорить просто, это анализ исходного кода без непосредственного запуска этого кода. Когда вы пробегаете глазами по коду, чтобы убедиться, что с ним всё ок — это тоже статический анализ 🙂. Противоположный подход — динамический анализ — предполагает, что код будет запускаться и анализироваться во время исполнения. Примером динамического анализа кода являются тесты, но о них поговорим в другой раз.
Обычно статический анализ строится на основе набора правил, например:
- Длина строки не должна превышать 120 символов;
- Нельзя вызывать
defer
внутриfor
; - и т.д.
Программы, которые выполняют статический анализ, называют линтерами. Для Go написано уже очень много линтеров, каждый из которых специализируется на своём наборе проверок. Например, bodyclose проверяет, что разработчик не забыл закрыть тело ответа при отправке HTTP-запросов:
internal/httpclient/httpclient.go:13:13: response body must be closed
А wsl следит за тем, чтобы в коде были правильно расставлены пустые строки для повышения читаемости.
Линтеров для Go настолько много, что управлять всеми ими самостоятельно стало в один момент сложнее, чем проверять программы вручную. Поэтому следующим этапом стало появление металинтеров — программ, которые позволяют настроить и запустить большое количество линтеров из единого места. Одним из таких является golangci-lint
.
Установка
golangci-lint
можно установить как с помощью привычных пакетных менеджеров, так и с помощью go install
. Однако авторы рекомендуют всё-таки ставить с помощью пакетного менеджера.
Быстрый старт
После установки можно сразу пользоваться программой, используя настройки по умолчанию:
|
|
или
|
|
./...
означает, что линтер отработает на файлах в текущей директории, а также рекурсивно на всех файлах во всех подпапках текущей директории.
Настройка
У golangci-lint
хорошая конфигурация по умолчанию, однако она может подойти не для всех проектов. Поэтому можно положить кастомный конфиг-файл .golangci.yml
в папку, откуда вы запускаете golangci-lint
или явно передать путь до файла:
|
|
Сам конфигурационный файл может выглядеть примерно вот так:
|
|
Чаще всего будете редактировать конфиг в следующих ситуациях:
- Нужно выключить определённую проверку;
- Нужно добавить или убрать линтер;
- Нужно задать настройки для конкретного линтера;
- Нужно исключить файл или папку из анализа.
Давайте разберёмся с данными кейсами по порядку.
Выключение проверки
golangci-lint
позволяет исключить правила из вывода по шаблону. Например, многие команды отказываются от обязательного комментирования экспортируемых (т.е. публичных) типов, методов, полей, переменных и т.д. Отключить проверку на наличие комментария можно, добавив в конфиге в секцию issues.exclude
регулярное выражение, которому соответствует сообщению от линтера:
|
|
Либо включить исключения по умолчанию, если данная опция была принудительно выключена, т.к. данное исключение уже есть в наборе из коробки:
|
|
Все исключения по умолчанию можно посмотреть, вызвав команду golangci-lint run --help
:
# EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked
# EXC0002 golint: Annoying issue about not having a comment. The rare codebase has such comments
- (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)
# EXC0003 golint: False positive when tests are defined in package 'test'
- func name will be used as test\.Test.* by other packages, and that stutters; consider calling this
# EXC0004 govet: Common false positives
- (possible misuse of unsafe.Pointer|should have signature)
# EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore
- ineffective break statement. Did you mean to break out of the outer loop
# EXC0006 gosec: Too many false-positives on 'unsafe' usage
- Use of unsafe calls should be audited
# EXC0007 gosec: Too many false-positives for parametrized shell calls
- Subprocess launch(ed with variable|ing should be audited)
# EXC0008 gosec: Duplicated errcheck checks
- (G104|G307)
# EXC0009 gosec: Too many issues in popular repos
- (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)
# EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
- Potential file inclusion via variable
# EXC0011 stylecheck: Annoying issue about not having a comment. The rare codebase has such comments
- (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)
# EXC0012 revive: Annoying issue about not having a comment. The rare codebase has such comments
- exported (.+) should have comment( \(or a comment on this block\))? or be unexported
# EXC0013 revive: Annoying issue about not having a comment. The rare codebase has such comments
- package comment should be of the form "(.+)...
# EXC0014 revive: Annoying issue about not having a comment. The rare codebase has such comments
- comment on exported (.+) should be of the form "(.+)..."
# EXC0015 revive: Annoying issue about not having a comment. The rare codebase has such comments
- should have a package comment, unless it's in another file for this package
Если вам необходимо выключить правило не глобально, а в определённом файле, или даже на определённой строке, можно оставить в соответствующем месте директиву nolint
:
|
|
Конкретные линтеры хоть и не обязательно, но всё же желательно указать, чтобы случайно не пропустить другие ошибки.
Изменение списка линтеров
По умолчанию golangci-lint
использует небольшой набор из поддерживаемых линтеров. Чтобы включить какой-то из линтеров, например, вышеупомянутый bodyclose
, нужно добавить его в секцию linters.enable
:
|
|
Отдельно устанавливать при этом линтеры не нужно, они все зашиты внутрь golangci-lint
.
Настройка конкретного линтера
Кроме того, каждый линтер можно потюнить в том же файле. Например, чтобы поменять максимальную допустимую длину строки, которую проверяет lll
, в секции linter-settings.lll
нужно добавить следующее:
|
|
Т.е. нет необходимости хранить кучу конфигов для каждого линтера — всё можно настроить в одном единственном файле в едином формате!
Исключение файлов и папок
Чтобы исключить определённый файл или папку из проверки, нужно добавить его в секцию exclude_paths
:
|
|
Настройка golangci-lint в Visual Studio Code
Официальное расширение Go для Visual Studio Code умеет запускать golangci-lint
из коробки. Достаточно в settings.json
прописать следующие параметры:
|
|
При включенном флаге --fast
golangci-lint
будет исключать некоторые линтеры, работающие долго, чтобы повысить отзывчивость редактора.
Советы по внедрению линтеров в команде
Если вы собираетесь внедрить линтеры в проект, который только начался, скорее всего это будет просто. Но часто проблемы с качеством кода, и как следствие, необходимость внедрения анализа кода возникает в проектах с длинной и не всегда красивой историей. Для таких проектов есть несколько вариантов:
Включить
golangci-lint
с максимальным количеством линтеров, причём так, чтобы при наличии ошибок, сборка проекта не прошла. На самом деле, именно так часто команды с большой нажитой кодовой базой и поступают, стреляя себе в ногу. Ведь в итоге всем разработчикам со своих задач приходится переключаться на исправление ошибок, либо на линтер в конце концов забивают, делая его запуск опциональным. В общем, такой экстремальный вариант подходит, только если проект только начался, и кода написано не так много.Внедрять постепенно. Любое значительное изменение в процессе разработки почти всегда негативно влияет как на мотивацию и производительность команды, так и качество и правильность использования внедряемого инструмента или процесса. Поэтому такие изменения не должны быть резкими. Линтеры — не исключение. И тут есть несколько подходов:
Не включать все проверки сразу. Настройте базовый набор проверок, и после того, как все ошибки будут устранены, вводите новые.
Не включайте всю кодовую базу в скоуп проверок сразу. В первую очередь исправьте ошибки в самых критичных местах проекта, добавив остальные в исключения. По мере исправления ошибок сужайте «зону исключений».
Включите проверки только для нового кода. Если запустить
golangci-lint
с флагом--new-from-rev=HEAD~1
или просто--new
, проверки будут срабатывать только на последних изменениях в коде. Постепенно можно менять указатель на всё более старые коммиты.
Скорее всего вам стоит применять комбинацию из данных трёх способов, чтобы достичь наиболее плавного внедрения линтеров в проект.
Окружение для разработки
Хорошим тоном является прогон линтера разработчиком перед коммитом или созданием пулл-реквеста — это экономит время и коллегам, и вам. Чтобы начать это делать было комфортнее, необходимо избавить разработчика от мук по установке и настройке линтера на рабочей машине.
Наверное, самым популярным в сообществе Go инструментом по автоматизации различных разработческих задач является GNU Make. Базовый Makefile с тасками по запуску линтера может выглядеть так:
|
|
Скопируйте его в ваш проект (или возьмите из репозитория), затем запустите в терминале команду:
|
|
Обратите внимание, что после выполнения команды сначала создалась папка bin
, а в ней — исполняемый файл golangci-lint
. Только после этого была запущена сама программа. Т.е. данная команда автоматически устанавливает локальную копию golangci-lint
в проект, и используется именно она. Во-первых, это избавляет разработчика от необходимости устанавливать golangci-lint
вручную. Во-вторых, так вы можете использовать разные версии данной программы в разных проектах.
Полезные ссылки
- Официальный сайт
golangci-lint
- Пример Makefile с тасками для запуска линтеров
- Илья Исаев, автор
golangci-lint
«Линтеры в Go: как их готовить» @ GopherCon Russia 2019