Хороший разработчик обычно стремится написать код, который не просто работает, но работает стабильно. Код, который легко читать и сопровождать. Достичь такого высокого качества кода кроме тестов также помогают единый стиль кода, хорошая документация, а также простота и изящность, как на уровне всей системы, так и на уровне отдельных модулей и функций.

Считается, что соответствие кода принятым в команде стандартам качества проверяет как сам разработчик, так и его коллеги на код-ревью. Проблема только в том, что все мы люди, и всегда можем что-то упустить. А иногда (на самом деле очень часто), разработчики закрывают глаза на те моменты, которые кажутся им незначительными. В связи с этим придумали статический анализ кода. Если говорить просто, это анализ исходного кода без непосредственного запуска этого кода. Когда вы пробегаете глазами по коду, чтобы убедиться, что с ним всё ок — это тоже статический анализ 🙂. Противоположный подход — динамический анализ — предполагает, что код будет запускаться и анализироваться во время исполнения. Примером динамического анализа кода являются тесты, но о них поговорим в другой раз.

Обычно статический анализ строится на основе набора правил, например:

  • Длина строки не должна превышать 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. Однако авторы рекомендуют всё-таки ставить с помощью пакетного менеджера.

Быстрый старт

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

1
golangci-lint run

или

1
golangci-lint run ./...

./... означает, что линтер отработает на файлах в текущей директории, а также рекурсивно на всех файлах во всех подпапках текущей директории.

Настройка

У golangci-lint хорошая конфигурация по умолчанию, однако она может подойти не для всех проектов. Поэтому можно положить кастомный конфиг-файл .golangci.yml в папку, откуда вы запускаете golangci-lint или явно передать путь до файла:

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

Сам конфигурационный файл может выглядеть примерно вот так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Options for analysis running.
run:
  # The default concurrency value is the number of available CPU.
  concurrency: 4
  # Timeout for analysis, e.g. 30s, 5m.
  # Default: 1m
  timeout: 5m
  # Exit code when at least one issue was found.
  # Default: 1
  issues-exit-code: 2
  # Include test files or not.
  # Default: true
  tests: false
  # List of build tags, all linters use it.
  # Default: [].
  build-tags:
    - mytag
  # Which dirs to skip: issues from them won't be reported.
  # Can use regexp here: `generated.*`, regexp is applied on full path.
  # Default value is empty list,
  # but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
  # "/" will be replaced by current OS file path separator to properly work on Windows.
  skip-dirs:
    - src/external_libs
    - autogenerated_by_my_lib
  # Enables skipping of directories:
  # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
  # Default: true
  skip-dirs-use-default: false
  # Which files to skip: they will be analyzed, but issues from them won't be reported.
  # Default value is empty list,
  # but there is no need to include all autogenerated files,
  # we confidently recognize autogenerated files.
  # If it's not please let us know.
  # "/" will be replaced by current OS file path separator to properly work on Windows.
  skip-files:
    - ".*\\.my\\.go$"
    - lib/bad.go
  # If set we pass it to "go list -mod={option}". From "go help modules":
  # If invoked with -mod=readonly, the go command is disallowed from the implicit
  # automatic updating of go.mod described above. Instead, it fails when any changes
  # to go.mod are needed. This setting is most useful to check that go.mod does
  # not need updates, such as in a continuous integration and testing system.
  # If invoked with -mod=vendor, the go command assumes that the vendor
  # directory holds the correct copies of dependencies and ignores
  # the dependency descriptions in go.mod.
  #
  # Allowed values: readonly|vendor|mod
  # By default, it isn't set.
  modules-download-mode: readonly
  # Allow multiple parallel golangci-lint instances running.
  # If false (default) - golangci-lint acquires file lock on start.
  allow-parallel-runners: false
  # Define the Go version limit.
  # Mainly related to generics support in go1.18.
  # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.17
  go: '1.18'

Чаще всего будете редактировать конфиг в следующих ситуациях:

  • Нужно выключить определённую проверку;
  • Нужно добавить или убрать линтер;
  • Нужно задать настройки для конкретного линтера;
  • Нужно исключить файл или папку из анализа.

Давайте разберёмся с данными кейсами по порядку.

Выключение проверки

golangci-lint позволяет исключить правила из вывода по шаблону. Например, многие команды отказываются от обязательного комментирования экспортируемых (т.е. публичных) типов, методов, полей, переменных и т.д. Отключить проверку на наличие комментария можно, добавив в конфиге в секцию issues.exclude регулярное выражение, которому соответствует сообщению от линтера:

1
2
3
4
5
# ...
issues:
	exclude:
		- "(comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)"
# ...

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

1
2
3
4
# ...
issues:
	exclude-use-default: true
# ...

Все исключения по умолчанию можно посмотреть, вызвав команду 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:

1
2
3
func Sum(a, b int) int { //nolint:revive,golint
	return a + b
}

Конкретные линтеры хоть и не обязательно, но всё же желательно указать, чтобы случайно не пропустить другие ошибки.

Изменение списка линтеров

По умолчанию golangci-lint использует небольшой набор из поддерживаемых линтеров. Чтобы включить какой-то из линтеров, например, вышеупомянутый bodyclose, нужно добавить его в секцию linters.enable:

1
2
3
4
5
6
7
# ...
linters:
	enable:
		# ...
		- bodyclose
		# ...
# ...

Отдельно устанавливать при этом линтеры не нужно, они все зашиты внутрь golangci-lint.

Настройка конкретного линтера

Кроме того, каждый линтер можно потюнить в том же файле. Например, чтобы поменять максимальную допустимую длину строки, которую проверяет lll, в секции linter-settings.lll нужно добавить следующее:

1
2
3
4
5
6
7
# ...
linter-settings:
	# ...
	lll:
		line-length: 70 # default is 120
	# ... 
# ...

Т.е. нет необходимости хранить кучу конфигов для каждого линтера — всё можно настроить в одном единственном файле в едином формате!

Исключение файлов и папок

Чтобы исключить определённый файл или папку из проверки, нужно добавить его в секцию exclude_paths:

1
2
3
4
5
6
7
8
9
# ...
linters:
	# ...
	exclude_paths:
		# ...
		- "vendor"
		- "*.gen.go"
		# ...
# ...

Настройка golangci-lint в Visual Studio Code

Официальное расширение Go для Visual Studio Code умеет запускать golangci-lint из коробки. Достаточно в settings.json прописать следующие параметры:

1
2
3
4
5
6
{	
	"go.lintTool": "golangci-lint",
	"go.lintFlags": [
		"--fast"
	]
}

При включенном флаге --fast golangci-lint будет исключать некоторые линтеры, работающие долго, чтобы повысить отзывчивость редактора.

Советы по внедрению линтеров в команде

Если вы собираетесь внедрить линтеры в проект, который только начался, скорее всего это будет просто. Но часто проблемы с качеством кода, и как следствие, необходимость внедрения анализа кода возникает в проектах с длинной и не всегда красивой историей. Для таких проектов есть несколько вариантов:

  1. Включить golangci-lint с максимальным количеством линтеров, причём так, чтобы при наличии ошибок, сборка проекта не прошла. На самом деле, именно так часто команды с большой нажитой кодовой базой и поступают, стреляя себе в ногу. Ведь в итоге всем разработчикам со своих задач приходится переключаться на исправление ошибок, либо на линтер в конце концов забивают, делая его запуск опциональным. В общем, такой экстремальный вариант подходит, только если проект только начался, и кода написано не так много.

  2. Внедрять постепенно. Любое значительное изменение в процессе разработки почти всегда негативно влияет как на мотивацию и производительность команды, так и качество и правильность использования внедряемого инструмента или процесса. Поэтому такие изменения не должны быть резкими. Линтеры — не исключение. И тут есть несколько подходов:

    1. Не включать все проверки сразу. Настройте базовый набор проверок, и после того, как все ошибки будут устранены, вводите новые.

    2. Не включайте всю кодовую базу в скоуп проверок сразу. В первую очередь исправьте ошибки в самых критичных местах проекта, добавив остальные в исключения. По мере исправления ошибок сужайте «зону исключений».

    3. Включите проверки только для нового кода. Если запустить golangci-lint с флагом --new-from-rev=HEAD~1 или просто --new, проверки будут срабатывать только на последних изменениях в коде. Постепенно можно менять указатель на всё более старые коммиты.

    Скорее всего вам стоит применять комбинацию из данных трёх способов, чтобы достичь наиболее плавного внедрения линтеров в проект.

Окружение для разработки

Хорошим тоном является прогон линтера разработчиком перед коммитом или созданием пулл-реквеста — это экономит время и коллегам, и вам. Чтобы начать это делать было комфортнее, необходимо избавить разработчика от мук по установке и настройке линтера на рабочей машине.

Наверное, самым популярным в сообществе Go инструментом по автоматизации различных разработческих задач является GNU Make. Базовый Makefile с тасками по запуску линтера может выглядеть так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
PROJECT_DIR = $(shell pwd)
PROJECT_BIN = $(PROJECT_DIR)/bin
$(shell [ -f bin ] || mkdir -p $(PROJECT_BIN))
PATH := $(PROJECT_BIN):$(PATH)

GOLANGCI_LINT = $(PROJECT_BIN)/golangci-lint

.PHONY: .install-linter
.install-linter:
	### INSTALL GOLANGCI-LINT ###
	[ -f $(PROJECT_BIN)/golangci-lint ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_BIN) v1.46.2

.PHONY: lint
lint: .install-linter
	### RUN GOLANGCI-LINT ###
	$(GOLANGCI_LINT) run ./... --config=./.golangci.yml

.PHONY: lint-fast
lint-fast: .install-linter
	$(GOLANGCI_LINT) run ./... --fast --config=./.golangci.yml

Скопируйте его в ваш проект (или возьмите из репозитория), затем запустите в терминале команду:

1
make lint

Обратите внимание, что после выполнения команды сначала создалась папка bin, а в ней — исполняемый файл golangci-lint. Только после этого была запущена сама программа. Т.е. данная команда автоматически устанавливает локальную копию golangci-lint в проект, и используется именно она. Во-первых, это избавляет разработчика от необходимости устанавливать golangci-lint вручную. Во-вторых, так вы можете использовать разные версии данной программы в разных проектах.

Полезные ссылки