Go и контроль качества проекта

А начну я с громкого заявления: Go – это один из самых лучших языков программирования для командной разработки на данный момент. И дело тут ни в простоте языка, ни в простоте инфраструктуры, хотя и эти пункты очень важны. Основная же причина для такого заявления кроется в инструментах для статического анализа кода. При этом я не знаю ни одного другого языка, который позволял бы провести такой дотошный статический анализ по всем направлениям, начиная от стилистики кода, заканчивая потенциально опасными конструкциями и обработкой ошибок.

Основная проблема в командной разработке – разнородность уровня и, как следствие, разное качество производимого командой кода. Ревью, безусловно, позволяют в той или иной степени сгладить последствия разнородности и даже подтянуть уровень разработчиков, но работают они не так хорошо как хотелось бы. Кому-то может быть лень, кто-то устал, кто-то не заметил и в репозитории оказывается нечто, которому там не место. Можно ли это хоть как-то исправить в C++? Нет, нельзя. Можно ли эту проблему минимизировать в Go? Довольно легко, что я вчера и сделал.

В Git есть замечательный механизм хуков, которые отрабатывают в зависимости от внутренних событий. Меня интересовал pre-commit хук, позволяющий заблокировать коммит по результату выполнения скрипта. Использование этого хука купе с Go Meta Linter, агрегатором линтеров для Go, позволяет автоматически заблокировать коммит до исправления ошибок.

#!/bin/sh

if ! [ -x "$(command -v gometalinter)" ]; then
echo 'Error: gometalinter is not installed. Please install it first and execute `gometalinter -i`'
exit 1
fi

lint_errors=$(gometalinter ./... --vendor -j 5)
if [[ $? != 0 ]]; then
echo 'Error: gometalinter checks had failed. Please execute `gometalinter ./... --vendor` first and fix ALL issues'
exit 1
fi

Дело остается за малым – поместить файл с хуком в директорию .git/hooks проекта, что я сделал при помощи нашего сборочного скрипта. Да, нам пришлось написать такой скрипт поверх стандартных Go команд типа go build так как есть пре- и пост- шаги которые необходимо предпринять в процессе сборки, а никакого CMake-подобного механизма в Go нет. Хотя, что уж тут, поверх CMake писать аналогичный скрипт тоже приходится.

Эксперименты с Rust

В течении последних лет я время от времени с интересом поглядывал на Rust, наверное, с версии 0.3. За это время язык претерпел много изменений, за счет чего он то нравился, то совсем не нравился, то вызывал сомнения. Но не смотреть на него было никак нельзя, так как языков, подходящих для “коробочной” разработки и не генерирующих байт-код, по пальцам одной руки пересчитать можно и еще свободные пальцы останутся.

Плотно поработав с Go в течении последнего года, я вновь подумал о том, что Rust довольно интересен, и стоит попробовать решить с его помощью практическую задачу. Мне как раз нужно было приложение для генерации оператора вывода всех членов класса/структуры в поток, на этой задаче я и решил остановиться. Мелочь, конечно, но полезно, нужно лично мне и позволяет сделать как минимум поверхностные выводы о пригодности языка для повседневного использования.

Подготовка к работе

Rust, конечно, не может похвастаться таким изобилием редакторов, как C++ или Go, но для относительно квалифицированного разработчика, привыкшего к командной строке, того, что есть, более чем достаточно. При этом управление зависимостями, создание проектов, установка внешних утилит выше всяких похвал.

Установка

Сам компилятор и набор утилит типа cargo устанавливается и обновляется очень легко и удобно, особенно на UNIX-ах. Всего одна консольная команда в соответствии с официальной документацией. Windows никогда особо дружелюбным для разработчиков не был, хотя и тут разработчики Rust постарались и сделали как поддержку GNU toolchains, так и поддержку MSVC. Всё заработало “из коробки”, что несказанно меня удивило. Ощущение зрелости как минимум системы доставки и развертывания однозначно присутствует.

Редактор

В моём случае с редактором было всё просто: взять Vim да установить пару-тройку дополнений к нему. Подсветка и форматирование работают отлично, автодополнение, базирующееся на racer, работает сносно. Как я понял, все остальные редакторы и IDE также используют racer для автодополнения и ожидать какого-то чуда за пределами VIM не следует. Понадобилось всего два плагина: rust.vim и vim-racer. В обоих случаях они снабжены достаточно подробной документацией и никакой сложности установка и настройка не вызывает.

Особо хотелось бы отметить наличие rustfmt, который позволяет форматировать код в едином стиле. Впервые я проникся любовью к такого рода приложениям, начав работать с Go, где весь код автоматически форматируется одним единственным правильным способом. В командной работе практика оказалась невероятно удобной, так как из “стандарта кодирования” команды полностью исчезает пункт про форматирование, при этом весь код выглядит одинаково, что облегчает понимание и работу с чужими фрагментами/модулями. После этого я внедрил у нас использование clangformat на C++ проектах (правда уже с единым набором правил для форматирования), что также сделало разработку ощутимо комфортнее.

Также в Vim большой популярностью пользуется плагин Tagbar, который не умеет работать с Rust из коробки, но это очень легко исправляется.

Еще одним потенциально полезным компонентом экосистемы Rust мне показался cargo-outdated, позволяющий отслеживать устаревшие зависимости. Ну просто мечта разработчика C++ какая-то, особенно учитывая то, что сторонние компоненты (дополнения) Rust устанавливаются при помощи cargo очень простым способом:

cargo install rustfmt
cargo install racer
cargo install cargo-outdated

Создание проекта

Я не раз сталкивался с мнением, что Rust можно полюбить хотя бы за наличие Cargo в нём. Попробовал и не могу не согласиться: всё очень просто и удобно.

cargo new --bin ddump-gen

И у меня есть новый проект, в котором, путем добавления всего двух строчек, я могу работать с libclang:

[dependencies]
clang = "0.16.0"

Это интересно само по себе хотя бы тем, что изначально я порывался написать тот же проект на C++, но бросил уже на стадии написания cmake-файла для этой же самой libclang.

Разработка

Теперь о самом главном: насколько удобно писать и поддерживать код на Rust. Для меня вопрос показался относительно сложным и полностью зависящим от текущих привычек и рабочих инструментов.

Доступные библиотеки

То, что Rust активно пиарится Mozilla и Samsung в течении последних лет, очень положительно сказалось на количестве доступных новых библиотек, а также оберток для уже существующих библиотек из мира C/C++. В результате очень быстро подобралась подходящая обертка над libClang, был найден удобный парсер командной строки и подходящий логгер. Довольно удивительно, но на решение тех же вопросов на C++ я бы потратил даже больше времени, чем в совершенно незнакомой экосистеме.

Про язык

С самим языком всё не столь однозначно и прекрасно. Наверное, если бы не опыт последних месяцев с Go, я бы просто подивился тому, почему же так неудобно обрабатывать ошибки, и отложил бы эксперименты до “лучших времен”. Но вот после того, как желание обязательно иметь в языке исключение прошло, оказалось, что с кодами возврата, особенно при наличии сопоставления с образцом, жить можно довольно комфортно.

Собственно говоря, к чему весь этот разговор про обработку ошибок. При отсутствии исключений в языке размер кода растет, так как, во-первых, появляется необходимость проверять коды возврата и не ожидать, что “исполнилось корректно либо выполнение было прервано”, а во-вторых, теряется возможность сделать обобщенную обработку нескольких потенциальных ошибок в одном блоке. Если задуматься над причиной, то её можно сформулировать как “мне нужно писать больше кода, но лень”. Есть ли в такой необходимости что-то хорошее? Опыт с Go подсказывает, что есть! Адресная обработка ошибок, т.е. сразу после возникновения ошибочной ситуации, обычно более качественно продумана и лучше покрыта диагностикой по сравнению с обобщенными исключениями. Кроме того, в стандартной библиотеке Rust пошли чуть дальше и добавили немного синтаксического сахара. Особо полезными, на мой взгляд, являются функции семейства unwrap(). Например, я точно знаю, что библиотека разбора аргументов командной строки не пропустит пустого значения для параметра SRC_FILE, и могу смело писать:

let path = matches.value_of("SRC_FILE").unwrap();

Смело потому, что попытка unwrap() для None обернется паникой и аварийным завершением приложения. Но для работы с такими ситуациями есть не менее удобный unwrap_or()

let std = matches.value_of("STD_VER").unwrap_or("11");

Среди относительно новых изменений языка числится `?`-синтаксис, которым я так и не решился воспользоваться, так как, на мой взгляд, он делает код просто невероятно сложно поддерживаемым и является злом по определению. Лучше уж отдать предпочтение макросу try!(), который делает то же самое, но более очевидным путем, а еще лучше либо явно звать unwrap(), либо писать полную обработку на базе match.

Ужас прошлых версий с временем жизни объектов разработчики спрятали, и теперь код можно, обычно, писать, особо не задумываясь о том, как удовлетворить компилятор. В версиях около 1.0 это был невероятно сильный отталкивающий фактор, так как вместо решения проблемы ты занимался удовлетворением хотелок компилятора буквально с первой линии кода. Само собой, правила никуда не делись, но компилятор стал ощутимо умнее.

В то же время мне сильно не нравится количество смысловой нагрузки на одну линию кода. Даже в простейшем примере выше с unwrap_or() видно, что тут упаковано и создание переменной с присвоением ей результат вызова функции, и условная проверка с возвратом значения по умолчанию, если функция ничего не вернула. Многовато для одной строки далеко не самого сложного кода на Rust.

Кому может быть интересен

В интернете можно найти довольно много мнений и споров на тему того, является ли Rust “убийцей C++” или нет. Смешные дискуссии, особенно с учетом того, сколько своих “убийц” C++ пережил. Поэтому я не буду говорить о том, кого же Rust вытеснит, только время рассудит, но вот кому и когда он может пригодиться, уже более интересно.

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

По моим ощущениям, при помощи современного Rust проблемы могут быть решены как минимум не хуже, чем при помощи современного C++, и более качественно, чем на современном Go. Так, для C++ программиста, активно использующего C++11 с STL и BOOST в повседневной работе, Rust может показаться ограниченным языком, особенно за счет куда как более бедной системы метапрограммирования и особенностей обработки ошибок. В то же время, для разработчика, активно использующего C++ с Qt или тот же Go, всё должно быть наоборот, приятно и комфортно. Если говорить про порог входа, то, с одной стороны, он однозначно выше, чем в случае с Go, начать писать на котором можно через 3-4 дня активного изучения документации и лучших практик. С другой стороны, информация, с которой придется ознакомиться, куда менее объемная и запутанная, чем даже минимальное вводное чтиво по C++.

Для себя я сделал вывод, что буду и дальше использовать Rust в “домашних проектах” и, при возможности, протолкну его продуктовую разработку в команде. Жизнь будет проще, жизнь будет веселее!