Главная » Хабрахабр » Статический анализ в Go: как мы экономим время при проверке кода

Статический анализ в Go: как мы экономим время при проверке кода

Меня зовут Сергей Рудаченко, я техлид в компании Roistat. Привет, Хабр. Они разрабатываются несколькими командами, поэтому нам понадобилось задать жесткую планку качества кода. Последние два года наша команда переводит различные части проекта в микросервисы на Go. Для этого мы используем несколько инструментов, в этой статье речь пойдет об одном из них — о статическом анализе.

Эта статья расскажет о его пользе, кратко опишет популярные инструменты и даст инструкции по внедрению. Статический анализ — процесс автоматической проверки исходного кода при помощи специальных утилит. Её стоит читать, если вы не сталкивались с подобными инструментами вовсе или используете их несистематически.

Для нас это удобное название простых инструментов для статического анализа. В статьях по этой теме часто встречается термин «линтер». Задача линтера — поиск простых ошибок и некорректного оформления.

Зачем нужны линтеры?

Ошибки, пропущенные на ревью, — это потенциальные баги. Работая в команде, вы, скорее всего, выполняете ревью кода. Ошиблись в приведении типов или обратились к nil map — еще хуже, бинарник упадет с panic. Пропустили необработанный error — не получите информативного сообщения и будете искать проблему вслепую.

Если в вашей голове нет компилятора, часть проблем все равно пройдет на бой. Описанные выше ошибки можно добавить в Code Conventions, но найти их при чтении пулл реквеста не так просто, ведь ревьюеру придется вчитываться в код. На дистанции поддержка такого кода станет дороже. Кроме того, поиск мелких ошибок отвлекает от проверки логики и архитектуры. Мы пишем на статически типизированном языке, странно этим не воспользоваться.

Популярные инструменты

Они предоставляют функции для разбора синтаксиса .go файлов. Большинство утилит для статического анализа используют пакеты go/ast и go/parser. Стандартный поток выполнения (например, для утилиты golint) такой:

  • загружается список файлов из требуемых пакетов
  • для каждого файла выполняется parser.ParseFile(...) (*ast.File, error)
  • запускается проверка поддерживаемых правил для каждого файла или пакета
  • проверка проходит по каждой инструкции, например, вот так:

f, err := parser.ParseFile(/* ... */) ast.Walk(func (n *ast.Node) }
}, f)

Это более сложный способ анализа кода, который работает с потоком выполнения, а не синтаксическими конструкциями. Помимо AST существует Single Static Assignment (SSA). В этой статье мы не будем рассматривать его подробно, можете почитать документацию и взглянуть на пример утилиты stackcheck.

Далее будут рассмотрены только популярные утилиты, которые выполняют полезные для нас проверки.

gofmt

Соответствие стилю для нас обязательное требование, поэтому проверка gofmt включена во всех наших проектах. Это стандартная утилита из пакета go, которая проверяет соответствие стилю и может автоматически его исправлять.

typecheck

Ее запуск обязателен для проверки компилируемости, но не дает полных гарантий. Typecheck проверяет соответствие типов в коде и поддерживает vendor (в отличие от gotype).

go vet

Проверяет ряд типичных ошибок, например: Утилита go vet — часть стандартного пакета и рекомендована к использованию командой Go.

  • неправильное использование Printf и аналогичных функций
  • некорректные build теги
  • сравнение function и nil

golint

К сожалению, подробной документации нет, но по коду можно понять, что проверяется следующее: Golint разработан командой Go и проверяет код на основе документов Effective Go и CodeReviewComments.

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

Проверок много, они разбиты по группам: Сами разработчики представляют staticcheck как улучшенный go vet.

  • неправильные использования стандартных библиотек
  • проблемы с многопоточностью
  • проблемы с тестами
  • бесполезный код
  • проблемы производительности
  • сомнительные конструкции

gosimple

Специализируется на поиске конструкций, которые стоит упростить, например:

До (исходный код golint)

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"
}

Документация аналогична staticcheck и включает подробные примеры.

errcheck

О причинах подробно рассказано в обязательном к прочтению документе Effective Go. Возвращаемые функциями ошибки нельзя игнорировать. Errcheck не пропустит следующий код:

json.Unmarshal(text, &val)
f, _ := os.OpenFile(/* ... */)

gas

Находит уязвимости в коде: захардкоженные доступы, 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"

maligned

Maligned находит неоптимальную сортировку. В Go порядок полей в структурах влияет на потребление памяти. При таком порядке полей:

struct { a bool b string c bool
}

Структура займет в памяти 32 бита из-за добавления пустых битов после полей a и c.

image

Если мы поменяем сортировку и поставим два bool поля вместе, то структура займет всего 24 бита:

image

Оригинал картинки на stackoverflow

goconst

Goconst находит литералы и числа, которые встречаются в коде 2 раза и более. Магические переменные в коде не отражают смысл и усложняют чтение. Обратите внимание, часто даже единственное использование может быть ошибкой.

gocyclo

Gocycle показывает сложность для каждой функции. Мы считаем цикломатическую сложность кода важной метрикой. Можно вывести только функции, которые превышают указанное значение.

gocyclo -over 7 package/name

Мы выбрали для себя пороговое значение 7, поскольку не нашли кода с более высокой сложностью, который не требовал рефакторинга.

Мертвый код

Есть несколько утилит для поиска неиспользуемого кода, их функциональность может частично пересекаться.

  • ineffassign: проверяет бесполезные присвоения

func foo() error { var res interface{} log.Println(res) res, err := loadData() // переменная res дальше не используется return err
}

  • deadcode: находит неиспользуемые функции
  • unused: находит неиспользуемые функции, но делает это лучше, чем deadcode

func unusedFunc() { formallyUsedFunc()
} func formallyUsedFunc() {
}

Благодаря этому лишний код удаляется за один проход. В результате unused укажет сразу на обе функции, а deadcode только на unusedFunc. Также unused находит неиспользуемые переменные и поля структур.

  • varcheck: находит неиспользуемые переменные
  • unconvert: находит бесполезные приведения типов

var res int
return int(res) // unconvert error

Если нужна оптимизация, рекомендую использовать unused и unconvert. Если нет задачи экономить на времени запуска проверок, лучше запускать их все вместе.

Как это все удобно настроить

Проверка одного из наших сервисов размером ~8000 строк кода занимала больше двух минут. Запускать перечисленные выше инструменты последовательно неудобно: ошибки выдаются в разном формате, выполнение занимает много времени. Устанавливать утилиты тоже придется по-отдельности.

Goreporter рендерит отчет в html, а gometalinter пишет в консоль. Для решения этой проблемы есть утилиты-аггрегаторы, например goreporter и gometalinter.

Он умеет устанавливать все утилиты одной командой, запускать их параллельно и форматировать ошибки по шаблону. Gometalinter до сих пор используется в некоторых крупных проектах (например, docker). Время выполнения в упомянутом выше сервисе сократилось до полутора минут.

Аггрегирование работает только по точному совпадению текста ошибки, поэтому на выходе неизбежны повторяющиеся ошибки.

В мае 2018 года на гитхабе появился проект golangci-lint, который сильно превосходит gometalinter в удобстве:

  • время выполнения на том же проекте сократилось до 16 секунд (в 8 раз)
  • практически не встречаются дубликаты ошибок
  • понятный yaml конфиг
  • приятный вывод ошибок со строкой кода и указателем на проблему
  • не требуется устанавливать дополнительные утилиты

Program, в будущем планируется также переиспользовать дерево AST, о котором я писал в начале раздела Инструменты. Сейчас прирост в скорости обеспечивается переиспользованием SSA и loader.

В будущем конфиг будет изменяться, так что для продакшена рекомендуем заменить его на собственный. На момент написания статьи на hub.docker.com не было образа с документацией, поэтому мы сделали собственный, настроенный по нашим представлениям об удобстве. Для этого достаточно добавить в корневую директорию проекта файл .golangci.yaml (пример есть в репозитории 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.

Любой проект запускается без установленного окружения go. Во всех наших сервисах для разработки используется docker. Для запуска команд используем 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.

  1. У вас небольшой проект и мало зависимостей (или они находятся в репозитории)
  2. Для вас не проблема ждать выполнения команды git push несколько минут

Инструкция по настройке хуков: Gitlab, Bitbucket Server, Github Enterprise.

Мы поступаем именно так, добавляя запуск линтеров перед тестами. В остальных случаях лучше использовать CI и запретить мерж кода, в котором есть хоть одна ошибка.

Заключение

Однако, более важно другое: теперь мы большую часть времени можем обсуждать общую картину и архитектуру. Внедрение систематических проверок заметно сократило период ревью. Это позволяет думать о развитии проекта вместо затыкания дыр.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] UDB. Что же это такое?

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

Беспроводные технологии передачи звука на базе Bluetooth: что же лучше?

С развитием технологий так привычные всем «ламповые» аналоговые наушники уходят в историю – их всё больше вытесняют беспроводные собратья на базе Bluetooth. Современные смартфоны лишаются привычного разъёма в угоду влаго- и пылезащищённости. Разработчики выпускают всё новые версии протокола Bluetooth и ...