Pull to refresh
95.58
Skillfactory
Онлайн-школа IT-профессий

Как писать код на Go? Подход Google. Часть первая

Reading time11 min
Views4.9K
Original author: Google

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


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


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


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


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

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



Ясность


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


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


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



Что делает код? (назначение)


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


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

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


Почему код делает это? (Обоснованность)


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


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

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


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


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


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



Простота


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


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


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

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


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


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


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


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


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


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

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


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



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


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


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


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

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


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


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


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

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


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


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


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


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


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

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


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


// Плохо:
// 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)

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


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



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


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



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


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


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


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


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


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


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



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


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


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


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


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


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


Длина строки


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


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


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

    Именование


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


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


  • ощущаться бесполезным повторением;
  • выбираться без учёта контекста;
  • повторять понятия, которое уже используется.

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



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


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


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


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

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


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

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


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




Tags:
Hubs:
Total votes 7: ↑4 and ↓3+2
Comments1

Articles

Information

Website
www.skillfactory.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Skillfactory School