Рекомендации по стилю для проектов из Google с открытым исходным кодом


Руководство по стилю Go


Принципы стиля


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


  1. Ясность: Назначение и обоснованность кода должны быть понятны читателю
  2. Простота: Код должен выполнять свою задачу самым простым способом
  3. Лаконичность: Код должен содержать как можно меньше «воды»
  4. Сопровождаемость: Код должен быть написан так, чтобы его легко было редактировать
  5. Согласованность: Код должен согласоваться с более масштабной кодовой базой Google

Подробности — к старту курса по Backend-разработке на Go.



Ясность


Основная цель читаемости (readability) — сделать код понятным читателю.


Ясность достигается, в первую очередь, созданием чёткой системы имён, написанием полезных комментариев и эффективной организацией кода.


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



Какова фактическая задача кода? (назначение)


Go разработан так, чтобы назначение кода было понятно читателю. Если при чтении кода возможны неясности или для его понимания нужно «быть в теме», автору стоит потратить время и сделать назначение кода понятнее будущим читателям. В частности, этому поспособствуют:


  • «говорящие» имена переменных
  • дополнительные комментарии
  • разбиение кода на секции разделителями и комментариями
  • модульный подход с рефракторингом по методам/функциям

Ни один подход не является универсальным, но при разработке кода в Go ясность всегда должна быть в приоритете.


Почему код выполняет именно эту задачу? (обоснованность)


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


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

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


Учтите также, что некоторые попытки внести ясность (например, дополнительные комментарии) приводят к обратному результату. Такое возможно, когда они делают вид кода беспорядочным, повторяют то, что предельно ясно из самого кода или противоречат коду по своей сути. Проблемы вызывают также комментарии, которые нужно постоянно обновлять, чтобы их содержание соответствовало действительности. Это затрудняет сопровождение кода. Пусть код говорит сам за себя, где это возможно (для этого, например, полезно давать объектам «говорящие» имена). Это лучше, чем писать комментарии запредельного объёма. Кроме того, часто бывает полезнее писать комментарии, объясняющие не что делает код, а зачем и почему он это делает.


В общем и целом, кодовая база Google стремится к единообразию и согласованности. Но часто бывает, что отдельный код в ней не соответствуют этим требованиям (например, включает малознакомые паттерны). Таким решениям есть веские причины. Как правило, эти причины связаны с оптимизацией производительности. При этом важно указать читателю, на что обратить внимание в новом фрагменте кода.


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



Простота


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


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


В рамках кодовой базы Google Go простой код:


  • легко читается от начала и до конца
  • не предполагает, что читатель изначально знаком с принципом работы этого кода
  • не требует от читателя удержания всего прочитанного в памяти
  • не имеет лишних уровней абстракции
  • не даёт имён, которые привлекают внимание к чему-либо слишком очевидному
  • объясняет читателю логику изменений значений и решений
  • включает комментарии, объясняющие не что делает код, а зачем и почему он это делает, не уводя читателя от темы
  • имеет самостоятельную документацию
  • включает указания на ожидаемые ошибки и сбои при проверке работы этого кода
  • зачастую не может соответствовать принципам «умного кода» (clever code)

Между простотой кода и простотой использования API возможны компромиссы. К примеру, усложнить код может быть целесообразно, чтобы конечному пользователю было проще правильно вызвать API. И наоборот, может быть целесообразно немного усложнить действия конечного пользователя API, чтобы код оставался простым и понятным.


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


Принцип простоты кода не означает, что сложный код не может и не должен создаваться в Go. Мы стремимся лишь к тому, чтобы кодовая база не содержала избыточно сложных кодов. Если код становится сложным, это означает, что он будет сложнее для понимания и сопровождения. В идеале, такая ситуация требует сопроводительного комментария с обоснованием принятого решения и указанием того, на что следует обратить особое внимание. Код часто становится сложнее для понимания человеком, когда оптимизируется его производительность с точки зрения машины. Повышение производительности часто требует более сложного подхода, например, предопределения буфера (preallocating a buffer) и последующее его использование на протяжении всей «гоурутины» (goroutine). Когда это видит тот, кто редактирует код, он понимает, что проверяемый код оптимизирован, и это нужно учитывать при внесении любых изменений. Однако же, если сложность внесена искусственно, без явной в том необходимости, она создаёт необоснованные трудности всем, кому в дальнейшем приходится читать и сопровождать такой код.


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


Принцип простейшей механики


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


  1. Стремитесь обходиться базовыми конструкциями языка, если их достаточно для достижения вашей цели. К таким конструкциям относятся, например, канал (channel), слайс (slice), карта, цикл и структура.
  2. Если же их недостаточно, ищите нужные вам языковые средства в стандартной библиотеке. Это может быть, например, гипертекстовый клиент или движок шаблонизатора.
  3. И, наконец, поищите подходящую библиотеку ядра (core library) в кодовой базе Google. Только если это не удалось, вводите новую зависимость или создавайте свою собственную.

Рассмотрим, например, рабочий код, где есть переменная с привязкой к флагу (flag). Она имеет значение по умолчанию, которое должно переопределяться в ходе тестирования. Если мы не намерены тестировать программу в режиме командной строки (допустим, с помощью os/exec), проще и разумнее переопределить связанное значение напрямую, чем использовать flag.Set.


Аналогично, если фрагменту кода нужна проверка наличия во множестве (set membership check), для этого часто достаточно карты логических значений. Например, это может быть map[string]bool. Библиотеки с типами и функционалом наподобие множеств используются только там, где необходимы сложные операции, для которых карты неприменимы или заметно усложняют задачу.



Лаконичность


Лаконичный код Go содержит минимальное количество «воды». В нём легко выделить самые важные части, а структура кода и система имён служат подсказками при чтении кода.


Многие аспекты могут оттенять важные части кода в разные моменты чтения. Среди них:


  • дублируемый код
  • аутентичный синтаксис
  • неинтуитивные имена
  • лишние уровни абстракции
  • пустые области

Когда код дублируется, очень трудно понять разницу между почти одинаковыми фрагментами. Читателю приходится сравнивать похожие строки кода и выискивать различия. Хорошим примером кода, где важные детали выделены на фоне повторений, являются Table-Driven Tests. При этом выбор того, что включать в таблицу, а что нет, напрямую влияет на понятность этой таблицы.


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


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


// Хорошо:
if err := doSomething(); err != nil {
    // ...
}

И если фрагмент кода очень похож на приведённый выше, читатель может и не заметить, что есть небольшое отличие. В подобном случае разумно «забустить» сигнал проверки ошибки, добавив привлекающий внимание комментарий.


// Хорошо:
if err := doSomething(); err == nil { // if NO error
    // ...
}


Сопровождаемость


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


Хорошо сопровождаемый код:


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

При использовании таких абстракций, как интерфейсы и типы, которые по определению вырывают информацию из контекста, важно убедиться, что они дают достаточную пользу. Мейнтейнеры и IDE могут напрямую обращаться к определению метода и отображать соответствующую документацию, если применяется конкретный тип. Если же это не так, они могут обратиться только к определению интерфейса. Интерфейсы — мощный инструмент, но за него приходится платить. Для правильного использования интерфейса при сопровождении может потребоваться знание специфики базовой реализации. Эту специфику необходимо объяснять в документации по интерфейсу или в точке вызова (call-site).


Сопровождаемость кода также предполагает, что важные детали кода не «завуалированы» там, где их легко пропустить. К примеру, для понимания приведённых ниже строк кода, нужно подметить факт наличия или отсутствия всего одного символа:


// Плохо:
// The use of = instead of := can change this line completely.
if user, err = db.UserByID(userID); err != nil {
    // ...
}

// Плохо:
// The ! in the middle of this line is very easy to miss.
leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0))

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


// Хорошо:
u, err := db.UserByID(userID)
if err != nil {
    return fmt.Errorf("invalid origin user: %s", err)
}
user = u

// Хорошо:
// Gregorian leap years aren't just year%4 == 0.
// See https://en.wikipedia.org/wiki/Leap_year#Algorithm.
var (
    leap4   = year%4 == 0
    leap100 = year%100 == 0
    leap400 = year%400 == 0
)
leap := leap4 && (!leap100 || leap400)

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


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


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


При структурировании кода стоит задуматься о том, как может быть изменён этот код со временем. Если при каком-то из возможных подходов вносить изменения будет проще и безопаснее, это будет хороший компромисс, даже если это несколько усложнит конструкцию кода.



Согласованность


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


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


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


Ключевые рекомендации


Эти рекомендации объединяют самые важные аспекты стиля Go, которым должен соответствовать любой код Go. Ожидается, что эти принципы будут усвоены и соблюдены к тому моменту, когда будет обеспечена читаемость кода. Мы не предполагаем, что они будут часто меняться, и новые дополнения должны будут соответствовать высокому уровню.


Ниже приведена расширенная версия рекомендаций Effective Go, которые признаны базовыми для языка Go всемирным комьюнити.



Форматирование


Все файлы исходного кода Go должны выводиться в формате инструмента gofmt. Этот формат поддерживается в кодовой базе Google при проверке перед отправкой кода (presubmit check). Как правило, генерируемый код должен иметь тот же формат (который задаётся, например, при помощи format.Source), поскольку и такой код индексируется системой Code Search.


Смешанные регистры


В исходном коде Go принято слитное написание без подчёркиваний (по образцу MixedCaps или mixedCaps). Иными словами, если имя состоит из нескольких слов, программисты Go используют «верблюжий» (camel case), а не «змеиный» регистр (snake case).


Это правило действует даже тогда, когда оно противоречит правилам синтаксиса других языков программирования. К примеру, экспортируемая постоянная будет записываться как MaxLength (а не MAX_LENGTH), а неэкспортируемая — как maxLength (а не max_length).


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


Длина строки


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


Кроме того, не разбивайте строку:


  • перед структурным изменением (indentation change), таким как объявление функции или условный оператор
  • если это разделит на части строковую переменную (например, URL-адрес)

    Именование


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


Имена не должны:


  • содержать лишнюю информацию
  • выбираться без учёта контекста
  • повторять очевидные концепции

Подробнее о выборе имён — в документе «Решения по стилю» .



Локальная согласованность


Если в «Руководстве по стилю» ничего не сказано о конкретном аспекте стиля, авторы кода вольны выбирать стиль по своему вкусу. Однако локальный код (код в одном файле или пакете, а иногда — также принадлежащий одной группе или директории) должен придерживаться согласованного подхода к решению каждой отдельной задачи.


Примеры правильного выбора локального стиля:


  • применение %s или %v при форматированном выводе ошибок
  • применение буферизированных каналов (buffered channels) вместо мьютексов

Примеры неправильного выбора локального стиля:


  • ограничение кода по длине строки
  • применение тестовых библиотек на основе утверждений (ассертов)

Если локальный стиль нарушает требования «Руководства по стилю», но это влияет лишь на читаемость одного файла, это обычно выявляется при критическом просмотре кода. Согласованные правки при этом остаются за пределами рассматриваемого списка изменений (CL). В этом случае разумно отправить сообщение об ошибке и следить за ходом исправлений.


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


Научим вас аккуратно работать с данными, чтобы вы прокачали карьеру и стали востребованным IT-специалистом. Новогодняя акция — скидки до 50% по промокоду HABR.




Комментарии (1)


  1. mickvav
    02.01.2023 01:58

    • языковые нюансы, такие как закрытие цикла захватом переменной цикла, прописанное в коде на много строк ниже цикла

    в оригинале -

    • A nuance in the language, e.g., a closure will be capturing a loop variable, but the closure is many lines away

    closure в данном контексте, как я понимаю, переводится как замыкание - по сути, локально-определенная функция, а не прекращение работы цикла.

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

    Дальше пока не читаю - исходный google-овский текст читается значимо проще.