5 способов писать эффективный код на Go: от названий переменных до архитектуры
- пятница, 29 марта 2024 г. в 00:00:16
Если вы задумывались, какие практики использовать, чтобы писать код на Go быстро и качественно, этот материал для вас. Руководитель группы разработки подсистем Геннадий Ковалёв и эксперт по разработке ПО Даниил Подольский в YADRO обсуждают пять способов повысить эффективность разработки в команде Go-программистов: они расскажут, как называть переменные, составлять документацию и продумывать архитектуру так, чтобы специалистам в команде и смежных отделах было легко работать с написанным кодом.
Статья будет полезна начинающим специалистам и командам, которые недавно работают вместе. Опытных разработчиков приглашаем в комментарии — расскажите, какие практики для повышения эффективности кода используете вы.
Руководитель группы разработки подсистем в YADRO
Наша команда работает над системой хранения данных TATLIN.UNIFIED, разрабатывает Control Path — подсистемы управления, благодаря которым разные составляющие СХД стабильно взаимодействуют друг с другом, а на «железке» загораются лампочки. Работы у нас много, поэтому мы ищем способы повысить продуктивность. На мой взгляд, разработку на Go можно назвать эффективной, когда она соответствует ожиданиям менеджера и заказчика, а также использует особенности языка. Ниже опишу критерии хорошего кода, к которому мы стремимся.
Проектному менеджеру важно, чтобы мы просто завершили проект. Продуктовый менеджер хочет, чтобы сделали качественно, быстро и дешево. В жизни не всегда получается соблюсти все три условия — и не всегда это нужно. На самом деле от разработчиков хотят не столько скорости, сколько предсказуемости. Чтобы продукт, который мы производим, дошел до заказчика в те сроки и с тем функционалом, которые согласовали в начале работы
У кода, который мы пишем, широкая «целевая аудитория»:
конечные потребители,
коллеги внутри компании,
мы сами — когда заходим в код через полгода и смотрим, что же мы там натворили.
Клиентам важно, чтобы продукт был качественным и содержал нужные характеристики. Коллегам хочется, чтобы код был понятным и сопровождаемым. QA-инженеры буквально «живут» в коде, когда пишут тесты, сотрудники отдела контроля качества определяют, где баг, а где фича, сервис-инженеры из службы техподдержки работают с кодом на стороне заказчика. Всем хочется работать с проектом, в котором не нужно лишний раз разбираться.
Здесь мы не будем рассматривать случаи, когда нам помешал внезапно найденный баг и мы переключились на его устранение. От этого не застраховаться. Зато с помощью некоторых практик можно избежать части проблем, которые замедляют работу над кодом. Например, на Go можно сократить цепочки зависимостей или написать код так, чтобы любой член команды смог набросить юнит-тест сразу, не тратя время на рефакторинг и негативные эмоции. А то бывает и так: пока бесился из-за зависимостей, рабочий день закончился.
Практики, перечисленные далее, помогут писать сопровождаемый код, с которым будет удобно работать коллегам. Вы можете наладить работу так, чтобы задачи не просто решались, а решались быстро, качественно и недорого — за счет меньшего количества ошибок и синхронизации между инженерами в одной команде.
Геннадий Ковалев: Первое, что мы сделали внутри команды. Никаких дискуссий о скобках — на go.dev уже все написано, нравится это разработчикам или нет. В нашей команде императивно применятся только gofmt, так как этот стиль упрощает работу с чужим кодом. На эту тему есть отличная цитата Роба Пайка:
"Gofmt's style is no one's favorite, yet gofmt is everyone's favorite" («Gofmt не должен нравиться кому-то, gofmt обязан нравиться всем»)
Эксперт по разработке ПО в команде Common Yadro Plaform (CYP) в YADRO
Если инженеры все же отказываются следовать правилам, то стоит настроить CI таким образом, чтобы он не пропускал несогласованный в команде код. Gofmt не регулирует весь синтаксис, поэтому, помимо CI, можно добавить в проект линтер WLS — так вы повысите читабельность кода. Более продвинутое решение — написать собственный линтер поверх RuleGuard, который есть в составе GoLangCI-Lint. К нему можно написать правила (rules), которые вы хотите зафиксировать в команде. Например, вы договорились, что переменная никогда не называется каким-то именем — добавьте в RuleGuard соответствующее правило.
Геннадий Ковалёв: Некоторые думают, что не имеет значения, как мы называем переменные, но в Go это работает не так. Внутри модуля есть папка package (пакет), а название переменной может состоять из одного или двух слов. Если название содержит одно слово, то, когда мы его читаем, понимаем, что:
находимся внутри одного пакета,
здесь нет внешней зависимости,
не нужно беспокоиться о том, где переменная объявлена и определена.
Если тип переменной состоит из имени пакета и имени переменной, мы понимаем, что она импортирована из внешнего пакета и объявлена там. В таком случае нужно давать название и пакету, и переменной, чтобы пара слова читалась как одно целое. Пример:
Мы также избегаем повторения слов. Например, если в пакете namespace я создаю функцию namespacenew, то называю ее коротко — namespace.new, а не namespace.namespacenew. Короткие и понятные названия без тавтологии помогают понять, что содержит переменная и в какой часть проекта находится. Больше не нужно тратить время на переключение в другие пакеты, чтобы понять семантику.
И напоследок классическое для всех языков программирования правило: в чужом коде используем только те значения переменных, которые уже объявлены. Когда вы называете одни и те же сущности одинаковыми именами, то экономите время коллег, которые будут разбираться в коде.
Даниил Подольский: Я как раз отношусь к тем, кто считает, что нейминг переменных в Go не очень важен — долго переменная в этом языке жить не должна. Однако то, как мы называем файл, играет большую роль. Чем очевиднее из названия, что лежит внутри, тем лучше. Если захотели назвать файл в два слова, используйте нижние подчеркивания и штатные суффиксы — например, _test. Слишком сложное имя с указанием на родительскую сущность говорит о том, что что вы плохо поработали над декомпозицией.
Геннадий Ковалёв: Для Go, как и для других языков, есть специальные утилиты, которые собирают комментарии по всему модулю и создают автодокументацию — например, Swagger.
Комментарии мы пишем прямо в коде и иногда даже на русском — для внутреннего использования достаточного одного предложения, если это, конечно, не тяжелая публичная библиотека. Как итог, в любой IDE мы можем навести курсор на функцию и понять, что она делает — для этого функция должна быть правильно продокументирована, например, в docdoc. Если до этого момента меня посылали по длинной цепочке зависимостей, то теперь я туда больше не пойду, мне работать надо.
Даниил Подольский: У меня, скажем так, маргинально-радикальная точка зрения, но я думаю, что вскоре она станет всеобщей. Дело в том, что документация на код не нужна почти никогда. Тезис про документацию придумали лет 30 назад, когда код читали, например, системные аналитики. Сегодня таких на свете нет, а документацию на код никто не читает, даже сами инженеры.
Кое-какие вещи документировать все же надо — а какие, вы узнаете в процессе ревью пул реквеста. Если коллега спрашивает, что означает некоторая часть кода, туда стоит добавить комментарий.
Геннадий Ковалёв: Тестирование тоже влияет на эффективность, главное — знать подход. В Go процесс работает так: если тестируем функцию, создаем файл с таким же именем и добавляем суффикс _test
к названию этого файла. А внутри файла пишем сам тест.
Разные виды тестирования лучше делить: мгновенные юнит-тесты должны делать разработчики, а тяжелые тесты, для которых нужно развернуть среду, лучше отдавать QA-инженерам. У нас интерфейс в среде разработки настроен так, что можно правой кнопкой мыши сгенерировать юнит-тест под любую функцию, который не содержит бутстрапов и миграций на базы данных.
За счет этого подхода IDE показывает покрытие кода тестами. Сохраняем файл, прогоняем юнит-тест — IDE сразу рисует, какой код покрыт. В результате, не отвлекаясь на другие зависимости, я вижу, что в моей функции проверилось, а что нет. На картинке видно, что строка 12 не покрыта — значит, надо написать еще один юнит-тест.
Правильная архитектура на Go (да и вообще везде) экономит много времени. Сразу отметим, архитектура кода — это спорная тема, у каждой компании свой взгляд на то, что это такое, где она начинается и кончается. Но мы расскажем об основных принципах — тех, что хорошо работают на Go.
Даниил Подольский: Во многом эти принципы соотносятся с книгой «Чистая архитектура». Это книжка Боба Мартина, или, как его называют разработчики, дяди Боба. Среди Go-программистов есть мнение, что он — чистый теоретик и никогда в жизни не писал код. Это не так. Роберт Мартин написал бесконечное количество кода, потому что у него была аутсорсинговая лавка по автоматизации enterprise в Штатах. Чтобы прорекламировать себя и свою контору, он выпустил несколько книг, в том числе «Чистую архитектуру». Важно, что «Чистая архитектура» — это не теория, а практическое изложение эмпирических правил, которые команда Мартина обнаружила, когда автоматизировала бизнес-процессы в разных американских компаниях. Соответственно, когда вы беретесь автоматизировать бизнес-процессы, вам стоит внимательно изучить этот материал.
Для работы с коллегами я вывел из «Чистой архитектуры» несколько правил, которые безотносительно к теории пытаюсь насаждать в команде.
Наружные слои могут использовать структуры внутренних слоев. Архитектура организована слоями. Обычно их три: транспортный, слой бизнес-логики и хранения. Нежелательно, чтобы структуры, методы, библиотеки, алгоритмы пересекали границы слоев. Но совсем без пересечения не получится, поэтому есть такое правило: наружные слои могут использовать структуры внутренних.
В бизнес-логике мы можем использовать структуры из внутренних слоев, но ни в коем случае не должны использовать структуры из транспортного слоя. Крайне нежелательно, чтобы сущности пересекали более одной границы слоя. Если в транспортном слое мы используем сущности из слоя хранения, это плохо. Делать так не надо. В принципе, можно представить ситуацию, когда этого не избежать. Например, когда нет никакой бизнес-логики и мы фактически на транспортном слое описываем операции с хранилищем. Но, во-первых, это редкая проблема. Во-вторых, подумайте еще. Может быть, вы просто плохо поняли задачу.
Обычно этих правил хватает, чтобы легко воспринимать архитектуру. По крайней мере легче, чем если это хаотичный результат деятельности нескольких программистов, которые друг с другом не советовались, а просто реализовывали очередную бизнес-задачу поверх кода, который был написан до них.
Геннадий Ковалёв: На мой взгляд, работу с кодом можно разбить на:
пользовательские кейсы (use cases) — как нашу программу используют пользователи,
глаголы — функции в коде,
существительные — структуры в Go, которые содержат свойства и характеристики.
Говорить об архитектуре можно в разных системах терминов. В этой статье мы используем термины из Domain Driven Design.
Например, в системе хранения данных есть лампочка. В программе описано существительное (сущность) «лампочка». У лампочки есть свойства: горит и не горит. Ее глагол (функция) — «зажечь лампочку». Пользователь говорит «хочу зажечь лампочку» — это use case. Такое разбиение программы помогает понять, где и что происходит в коде. Если ошибка в том, что лампочка не зажигается — мы понимаем, что проблема с глаголом, а значит, ищем проблему в функции.
Представим, что ищем в коде неисправную функцию. Чтобы быстрее найти баг, нужно заранее организовать понятное хранение каталогов и файлов. А еще, повторюсь, нужны короткие цепочки зависимостей, чтобы я не открывал тысячи соседних проектов, а видел только свой код — тот, который и нужно отлаживать.
В этом помогают интерфейсы. В объектно-ориентированных языках программирования с классической иерархией типов, таких как Java и C++, класс реализует интерфейс. В Go делается наоборот: инженер определяет интерфейсы, которые описывают внешние зависимости, нужные для текущего класса — так и разрываются длинные цепочки.
Получается простой порядок действий: нашли функцию, исправили ошибку, написали рядом юнит-тест.
Дальше запускаем юнит-тест на ту функцию, где обнаружили ошибку. Здесь тоже все должно быть организовано так, чтобы юнит-тест находился рядом с функцией. Так мы не тратим время на погружение в контекст других зависимостей и тестируем только то, что нам нужно.
Это далеко не все способы повышения эффективности разработки на Go, так как специфика языка позволяет по-разному улучшать процессы. Главное — согласовать изменения внутри команды и следовать общим правилам.
Используете ли вы практики, которые мы описали в статье? Может, у вас есть иные советы? Поделитесь ими в комментариях.