- PVSM.RU - https://www.pvsm.ru -
Привет. Меня зовут Сергей Рудаченко, я техлид в компании Roistat. Последние два года наша команда переводит различные части проекта в микросервисы на Go. Они разрабатываются несколькими командами, поэтому нам понадобилось задать жесткую планку качества кода. Для этого мы используем несколько инструментов, в этой статье речь пойдет об одном из них — о статическом анализе.
Статический анализ — процесс автоматической проверки исходного кода при помощи специальных утилит. Эта статья расскажет о его пользе, кратко опишет популярные инструменты и даст инструкции по внедрению. Её стоит читать, если вы не сталкивались с подобными инструментами вовсе или используете их несистематически.
В статьях по этой теме часто встречается термин «линтер». Для нас это удобное название простых инструментов для статического анализа. Задача линтера — поиск простых ошибок и некорректного оформления.
Работая в команде, вы, скорее всего, выполняете ревью кода. Ошибки, пропущенные на ревью, — это потенциальные баги. Пропустили необработанный error
— не получите информативного сообщения и будете искать проблему вслепую. Ошиблись в приведении типов или обратились к nil map — еще хуже, бинарник упадет с panic.
Описанные выше ошибки можно добавить в Code Conventions [1], но найти их при чтении пулл реквеста не так просто, ведь ревьюеру придется вчитываться в код. Если в вашей голове нет компилятора, часть проблем все равно пройдет на бой. Кроме того, поиск мелких ошибок отвлекает от проверки логики и архитектуры. На дистанции поддержка такого кода станет дороже. Мы пишем на статически типизированном языке, странно этим не воспользоваться.
Большинство утилит для статического анализа используют пакеты go/ast
и go/parser
. Они предоставляют функции для разбора синтаксиса .go файлов. Стандартный поток выполнения (например, для утилиты golint) такой:
parser.ParseFile(...) (*ast.File, error)
f, err := parser.ParseFile(/* ... */)
ast.Walk(func (n *ast.Node) {
switch v := node.(type) {
case *ast.FuncDecl:
if strings.Contains(v.Name, "_") {
panic("wrong function naming")
}
}
}, f)
Помимо AST существует Single Static Assignment (SSA). Это более сложный способ анализа кода, который работает с потоком выполнения, а не синтаксическими конструкциями. В этой статье мы не будем рассматривать его подробно, можете почитать документацию [2] и взглянуть на пример утилиты stackcheck [3].
Далее будут рассмотрены только популярные утилиты, которые выполняют полезные для нас проверки.
Это стандартная утилита из пакета go, которая проверяет соответствие стилю и может автоматически его исправлять. Соответствие стилю для нас обязательное требование, поэтому проверка gofmt включена во всех наших проектах.
Typecheck проверяет соответствие типов в коде и поддерживает vendor (в отличие от gotype). Ее запуск обязателен для проверки компилируемости, но не дает полных гарантий.
Утилита go vet [4] — часть стандартного пакета и рекомендована к использованию командой Go. Проверяет ряд типичных ошибок, например:
Golint разработан командой Go и проверяет код на основе документов Effective Go [5] и CodeReviewComments [6]. К сожалению, подробной документации нет, но по коду [7] можно понять, что проверяется следующее:
f.lintPackageComment()
f.lintImports()
f.lintBlankImports()
f.lintExported()
f.lintNames()
f.lintVarDecls()
f.lintElses()
f.lintRanges()
f.lintErrorf()
f.lintErrors()
f.lintErrorStrings()
f.lintReceiverNames()
f.lintIncDec()
f.lintErrorReturn()
f.lintUnexportedReturn()
f.lintTimeNames()
f.lintContextKeyTypes()
f.lintContextArgs()
Сами разработчики представляют staticcheck [8] как улучшенный go vet. Проверок много, они разбиты по группам:
Специализируется на поиске конструкций, которые стоит упростить, например:
До (исходный код golint [9])
func (f *file) isMain() bool {
if f.f.Name.Name == "main" {
return true
}
return false
}
После
func (f *file) isMain() bool {
return f.f.Name.Name == "main"
}
Документация [10] аналогична staticcheck и включает подробные примеры.
Возвращаемые функциями ошибки нельзя игнорировать. О причинах подробно рассказано в обязательном к прочтению документе Effective Go [11]. Errcheck не пропустит следующий код:
json.Unmarshal(text, &val)
f, _ := os.OpenFile(/* ... */)
Находит уязвимости в коде: захардкоженные доступы, sql инъекции и использование небезопасных хэш-функций.
Примеры ошибок:
// доступ со всех IP адресов
l, err := net.Listen("tcp", ":2000")
// потенциальная sql инъекция
q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", name)
q := "SELECT * FROM foo where name = " + name
// используйте другой хэш алгоритм
import "crypto/md5"
В Go порядок полей в структурах влияет на потребление памяти. Maligned находит неоптимальную сортировку. При таком порядке полей:
struct {
a bool
b string
c bool
}
Структура займет в памяти 32 бита из-за добавления пустых битов после полей a и c.
Если мы поменяем сортировку и поставим два bool поля вместе, то структура займет всего 24 бита:
Оригинал картинки на stackoverflow [12]
Магические переменные в коде не отражают смысл и усложняют чтение. Goconst находит литералы и числа, которые встречаются в коде 2 раза и более. Обратите внимание, часто даже единственное использование может быть ошибкой.
Мы считаем цикломатическую сложность [13] кода важной метрикой. Gocycle показывает сложность для каждой функции. Можно вывести только функции, которые превышают указанное значение.
gocyclo -over 7 package/name
Мы выбрали для себя пороговое значение 7, поскольку не нашли кода с более высокой сложностью, который не требовал рефакторинга.
Есть несколько утилит для поиска неиспользуемого кода, их функциональность может частично пересекаться.
func foo() error {
var res interface{}
log.Println(res)
res, err := loadData() // переменная res дальше не используется
return err
}
func unusedFunc() {
formallyUsedFunc()
}
func formallyUsedFunc() {
}
В результате unused укажет сразу на обе функции, а deadcode только на unusedFunc. Благодаря этому лишний код удаляется за один проход. Также unused находит неиспользуемые переменные и поля структур.
var res int
return int(res) // unconvert error
Если нет задачи экономить на времени запуска проверок, лучше запускать их все вместе. Если нужна оптимизация, рекомендую использовать unused и unconvert.
Запускать перечисленные выше инструменты последовательно неудобно: ошибки выдаются в разном формате, выполнение занимает много времени. Проверка одного из наших сервисов размером ~8000 строк кода занимала больше двух минут. Устанавливать утилиты тоже придется по-отдельности.
Для решения этой проблемы есть утилиты-аггрегаторы, например goreporter [14] и gometalinter [15]. Goreporter рендерит отчет в html, а gometalinter пишет в консоль.
Gometalinter до сих пор используется в некоторых крупных проектах (например, docker [16]). Он умеет устанавливать все утилиты одной командой, запускать их параллельно и форматировать ошибки по шаблону. Время выполнения в упомянутом выше сервисе сократилось до полутора минут.
Аггрегирование работает только по точному совпадению текста ошибки, поэтому на выходе неизбежны повторяющиеся ошибки.
В мае 2018 года на гитхабе появился проект golangci-lint, который сильно превосходит gometalinter в удобстве:
Сейчас прирост в скорости обеспечивается переиспользованием SSA и loader.Program [17], в будущем планируется также переиспользовать дерево AST, о котором я писал в начале раздела Инструменты.
На момент написания статьи на hub.docker.com [18] не было образа с документацией, поэтому мы сделали собственный, настроенный по нашим представлениям об удобстве. В будущем конфиг будет изменяться, так что для продакшена рекомендуем заменить его на собственный. Для этого достаточно добавить в корневую директорию проекта файл .golangci.yaml (пример [19] есть в репозитории golangci-lint).
PACKAGE=package/name docker run --rm -t
-v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE)
-w /go/src/$(PACKAGE)
roistat/golangci-lint
Этой командой можно проверить весь проект. Например, если он находится в ~/go/src/project
, то поменяйте значение переменной на PACKAGE=project
. Проверка работает рекурсивно по всем внутренним пакетам.
Обратите внимание, что эта команда работает корректно только при использовании vendor.
Во всех наших сервисах для разработки используется docker. Любой проект запускается без установленного окружения go. Для запуска команд используем Makefile и добавили в него команду lint:
lint:
@docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint
Теперь проверка запускается этой командой:
make lint
Есть простой способ заблокировать код с ошибками от попадания в мастер — создать pre-receive-hook. Он подойдет, если:
git push
несколько минутИнструкция по настройке хуков: Gitlab [20], Bitbucket Server [21], Github Enterprise [22].
В остальных случаях лучше использовать CI и запретить мерж кода, в котором есть хоть одна ошибка. Мы поступаем именно так, добавляя запуск линтеров перед тестами.
Внедрение систематических проверок заметно сократило период ревью. Однако, более важно другое: теперь мы большую часть времени можем обсуждать общую картину и архитектуру. Это позволяет думать о развитии проекта вместо затыкания дыр.
Автор: m1nor
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/open-source/282222
Ссылки в тексте:
[1] Code Conventions: https://habr.com/company/roistat/blog/352762/
[2] документацию: https://godoc.org/golang.org/x/tools/go/ssa
[3] stackcheck: https://github.com/danielmorsing/stackcheck
[4] go vet: https://golang.org/cmd/vet/
[5] Effective Go: https://golang.org/doc/effective_go.html
[6] CodeReviewComments: https://golang.org/wiki/CodeReviewComments
[7] по коду: https://github.com/golang/lint/blob/470b6b0bb3005eda157f0275e2e4895055396a81/lint.go#L195
[8] staticcheck: https://staticcheck.io/docs/staticcheck
[9] исходный код golint: https://github.com/golang/lint/blob/470b6b0bb3005eda157f0275e2e4895055396a81/lint.go#L368
[10] Документация: https://staticcheck.io/docs/gosimple#overview
[11] Effective Go: https://golang.org/doc/effective_go.html#errors
[12] Оригинал картинки на stackoverflow: https://stackoverflow.com/a/38034334
[13] цикломатическую сложность: https://ru.wikipedia.org/wiki/%D0%A6%D0%B8%D0%BA%D0%BB%D0%BE%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[14] goreporter: https://github.com/360EntSecGroup-Skylar/goreporter
[15] gometalinter: https://github.com/alecthomas/gometalinter
[16] docker: https://github.com/moby/moby
[17] loader.Program: https://godoc.org/golang.org/x/tools/go/loader#Program
[18] hub.docker.com: http://hub.docker.com
[19] пример: https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
[20] Gitlab: https://docs.gitlab.com/ee/administration/custom_hooks.html
[21] Bitbucket Server: https://confluence.atlassian.com/bitbucketserver/using-repository-hooks-776639836.html
[22] Github Enterprise: https://help.github.com/enterprise/2.13/admin/guides/developer-workflow/creating-a-pre-receive-hook-script/
[23] Источник: https://habr.com/post/413175/?utm_campaign=413175
Нажмите здесь для печати.