[Перевод] Практичный Go: советы по написанию поддерживаемых программ в реальном мире

habr.png

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

Для начала следует договориться, что значит лучшие практики для языка программирования. Здесь можно вспомнить слова Расса Кокса, технического руководителя Go:

Программная инженерия — то, что происходит с программированием, если добавить фактор времени и других программистов.


Таким образом, Расс различает понятия программирования и программной инженерии. В первом случае вы пишете программу для себя, во втором создаёте продукт, над которым со временем будут работать и другие программисты. Инженеры приходят и уходят. Команды растут или сокращаются. Добавляются новые функции и исправляются ошибки. Такова природа разработки программного обеспечения.
Возможно, среди вас я один из первых пользователей Go, но дело не в моём личном мнении. Эти базовые принципы лежат в основе самого Go:

  1. Простота
  2. Читаемость
  3. Продуктивность


Примечание. Обратите внимание, я не упомянул «производительность» или «параллелизм». Есть языки быстрее Go, но определённо они не могут сравниться по простоте. Есть языки, которые главным приоритетом ставят параллелизм, но они не сравняться ни по читаемости, ни по продуктивности программирования.

Производительность и параллелизм — важные атрибуты, но не настолько важные, как простота, читаемость и продуктивность.

Простота


«Простота — необходимое условие читаемости» — Эдсгер Дейкстра


Зачем стремиться к простоте? Почему важно, чтобы программы Go были простыми?

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

«Есть два способа проектирования ПО: первый — сделать его настолько простым, чтобы не было очевидных недостатков, а второй — сделать его настолько сложным, чтобы не было очевидных недостатков. Первое гораздо труднее» — Ч.Э. Р. Хоар


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

1.2. Удобочитаемость


«Читаемость — неотъемлемая часть ремонтопригодности» — Марк Рейнхольд, конференция по JVM, 2018


Почему важно, чтобы код был читаемым? Почему мы должны стремиться к читабельности?

«Программы следует писать для людей, а машины их всего лишь выполняют» — Хэл Абельсон и Джеральд Сассман, «Структура и интерпретация компьютерных программ»


Не только программы Go, но вообще всё программное обеспечение пишется людьми для людей. Тот факт, что машины тоже обрабатывают код, вторичен.

Однажды написанный код будет многократно прочитан людьми: сотни, а то и тысячи раз.

«Самый важный навык для программиста — умение эффективно передавать идеи» — Гастон Хоркера


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

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

Первый шаг к написанию поддерживаемого ПО — убедиться, что код понятен.

1.3. Продуктивность


«Дизайн — это искусство такой организации кода, чтобы он работал сегодня, но всегда поддерживал изменения» — Сэнди Мец


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

Ходит шутка, что язык Go разработали, пока компилировалась программа на C++. Быстрая компиляция — ключевая особенность Go и ключевой фактор привлечения новых разработчиков. Хотя компиляторы совершенствуются, но в целом минутные компиляции на других языках проходят за несколько секунд на Go. Так разработчики Go чувствуют себя столь же продуктивными, как программисты на динамических языках, но без проблем с надёжностью тех языков.

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

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

Когда разработчики Go говорят о масштабируемости языка, они имеют в виду именно продуктивность.


Первая тема, которую мы обсудим — идентификаторы, это синоним имён: названия переменных, функций, методов, типов, пакетов и так далее.

«Плохое имя — симптом плохого дизайна» — Дэйв Чейни


Учитывая ограниченный синтаксис Go, имена объектов оказывают огромное влияние на читаемость программ. Читаемость — ключевой фактор хорошего кода, поэтому выбор хороших имён имеет решающее значение.

2.1. Именуйте идентификаторы исходя из ясности, а не краткости


«Важно, чтобы код был очевидным. То, что можно сделать в одной строке, вы должны сделать в трёх» — Укия Смит


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

«Хорошее название как хорошая шутка. Если тебе нужно объяснять её, то уже не смешно» — Дэйв Чейни


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

  • Хорошее имя лаконично. Оно не обязательно должно быть самым коротким, но не содержит лишнего. У него высокое отношение сигнал/шум.
  • Хорошее имя является описательным. Оно описывает применение переменной или константы, а не содержимое. Хорошее имя описывает результат функции или поведение метода, а не реализацию. Назначение пакета, а не его содержимое. Чем точнее имя описывает вещь, которую идентифицирует, тем лучше.
  • Хорошее имя предсказуемо. По одному названию вы должны понимать, как будет использоваться объект. Названия должны быть описательными, но также важно следовать традиции. Вот что имеют в виду программисты Go, когда говорят «идиоматический».


Рассмотрим подробнее каждое из этих свойств.

2.2. Длина идентификатора


Иногда стиль Go критикуют за короткие имена переменных. Как сказал Роб Пайк, «программисты Go хотят идентификаторы правильной длины».

Эндрю Джерранд предлагает более длинными идентификаторами указывать на важность.

«Чем больше расстояние между объявлением имени и использованием объекта, тем длиннее должно быть имя» — Эндрю Джерранд


Таким образом, можно составить некоторые рекомендации:

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


Рассмотрим пример.

type Person struct {
        Name string
        Age  int
}

// AverageAge returns the average age of people.
func AverageAge(people []Person) int {
        if len(people) == 0 {
                return 0
        }

        var count, sum int
        for _, p := range people {
                sum += p.Age
                count += 1
        }

        return sum / count
}


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

Для сравнения, people объявляется в параметрах функции и живёт семь строк. То же самое относится к sum и count, поэтому они оправдывают свои более длинные имена. Читателю нужно просканировать больше кода, чтобы их найти: это оправдывает более отличительные имена.

Можно выбрать s для sum и c (или n) для count, но это сведёт важность всех переменных в программе к одному уровню. Можно заменить people на p, но возникнет проблема, как назвать переменную итерации for ... range. Единственный person будет выглядеть странно, потому что у короткоживущей переменной итерации получается более длинное название, чем у нескольких значений, из которых она выводится.

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


2.2.1. Главное — контекст


Важно понимать, что большинство советов по именованию зависят от контекста. Мне нравится говорить, что это принцип, а не правило.

В чём разница между идентификаторами i и index? Например, нельзя однозначно сказать, что такой код

for index := 0; index < len(s); index++ {
        //
}


принципиально более читаемый, чем

for i := 0; i < len(s); i++ {
        //
}


Я считаю, что второй вариант не хуже, потому что в данном случае область i или index ограничена телом цикла for, а дополнительная многословность мало что добавляет к пониманию программы.

А вот из этих функций какая более читабельна?

func (s *SNMP) Fetch(oid []int, index int) (int, error)


или

func (s *SNMP) Fetch(o []int, i int) (int, error)


В этом примере oid является аббревиатурой SNMP Object ID, а дополнительное сокращение до o заставляет при чтении кода перейти от документированной нотации к более короткой нотации в коде. Аналогично и сокращение index до i затрудняет понимание сути, поскольку в сообщениях SNMP значение sub каждого OID называется индексом.

Совет. Не комбинируйте длинные и короткие формальные параметры в одном объявлении.


2.3. Не называйте переменные по их типам


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

var usersMap map[string]*User


Что хорошего в этом объявлении? Мы видим, что это карта, и она имеет какое-то отношение к типу *User: вероятно, это хорошо. Но usersMap — действительно карта, а Go как статически типизированный язык не позволит случайно использовать такое название там, где требуется скалярная переменная, поэтому суффикс Map избыточен.

Рассмотрим ситуацию, когда добавляются другие переменные:

var (
        companiesMap map[string]*Company
        productsMap  map[string]*Products
)


Теперь у нас три переменные типа map: usersMap, companiesMap и productsMap, а все строки сопоставляются с разными типами. Мы знаем, что это карты, и мы также знаем, что компилятор выдаст ошибку, если мы попытаемся использовать companiesMap там, где код ожидает map[string]*User. В этой ситуации ясно, что суффикс Map не улучшает ясность кода, это просто лишние символы.

Предлагаю избегать любых суффиксов, которые напоминают тип переменной.

Совет. Если название users недостаточно ясно описывает суть, тогда usersMap тоже.


Этот совет также относится к параметрам функции. Например:

type Config struct {
        //
}

func WriteConfig(w io.Writer, config *Config)


Название config для параметра *Config избыточно. Мы и так знаем, что это *Config, тут же рядом написано.

В этом случае рассмотрим conf или c, если время жизни переменной достаточно короткое.

Если в какой-то момент в нашей области более одного *Config, то названия conf1 и conf2 менее содержательны, чем original и updated, так как последние труднее перепутать.

Примечание. Не позволяйте названиям пакетов украсть хорошие названия переменных.

Имя импортируемого идентификатора содержит название пакета. Например, тип Context в пакете context будет называться context.Context. Это делает невозможным использование в вашем пакете переменной или типа context.

func WriteLog(context context.Context, message string)

Такое не скомпилируется. Вот почему при локальном объявлении типов context.Context, например, традиционно используются имена вроде ctx.
func WriteLog(ctx context.Context, message string)


2.4. Используйте единый стиль именования


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

Например, если код проходит вокруг дескриптора базы данных, каждый раз при отображении параметра у него должно быть то же имя. Вместо всяческих сочетаний типа d *sql.DB, dbase *sql.DB, DB *sql.DB и database *sql.DB лучше использовать что-то одно:

db *sql.DB


Так проще понять код. Если вы видите db, то знаете, что это *sql.DB и она объявляется локально или предоставлена вызывающей стороной.

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

Примечание. Соглашение о коротких именах получателей в Go противоречит ранее озвученным рекомендациям. Это один из тех случаев, когда сделанный на раннем этапе выбор становится стандартным стилем, как использование CamelCase вместо snake_case.


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


Наконец, некоторые однобуквенные переменные традиционно ассоциируются с циклами и подсчётом. Например, i, j и k обычно являются индуктивными переменными в циклах for, n обычно ассоциируется со счётчиком или накапливающим сумматором, v является типичным сокращением value в кодирующей функции, k обычно используется для ключа карты, а s часто используется как сокращение для параметров типа string.

Как и в примере с db выше, программисты ожидают, что i является индуктивной переменной. Если они видят её в коде, то ожидают скоро встретить цикл.

Совет. Если у вас настолько много вложенных циклов, что вы исчерпали запас переменных i, j и k, то может следует разбить функцию на более мелкие единицы.


2.5. Используйте единый стиль деклараций


В Go есть минимум шесть разных способов объявления переменной

  • var x int = 1
    
  • var x = 1
    
  • var x int; x = 1
    
  • var x = int(1)
    
  • x := 1
    


Уверен, я ещё не все вспомнил. Разработчики Go, наверное, считают это ошибкой, но уже слишком поздно что-то менять. При таком выборе как обеспечить единообразный стиль?

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

  • При объявлении переменной без инициализации используйте var.
    var players int    // 0
    
    var things []Thing // an empty slice of Things
    
    var thing Thing    // empty Thing struct
    json.Unmarshall(reader, &thing)
    

    var действует как подсказка, что эта переменная намеренно объявлена как нулевое значение указанного типа. Это согласуется с требованием объявлять переменные на уровне пакета с помощью var в отличие от синтаксиса короткого объявления, хотя позже я приведу аргументы, что переменные уровня пакета вообще не следует использовать.
  • При объявлении c инициализацией используйте :=. Это даёт понять читателю, что переменная слева от := намеренно инициализируется.

    Чтобы объяснить почему, давайте рассмотрим предыдущий пример, но на этот раз специально инициализируем каждую переменную:

    var players int = 0
    
    var things []Thing = nil
    
    var thing *Thing = new(Thing)
    json.Unmarshall(reader, thing)
    


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

var players = 0

var things []Thing = nil

var thing = new(Thing)
json.Unmarshall(reader, thing)


Здесь players явно инициализируются в 0, что является избыточным, потому что начальное значение players в любом случае равно нулю. Поэтому лучше явно дать понять, что мы хотим использовать нулевое значение:

var players int


Что насчёт второго оператора? Мы не можем определить тип и написать

var things = nil


Потому что у nil нет типа. Вместо этого у нас выбор: или мы используем нулевое значение для среза…

var things []Thing


… или создаём срез с нулевым количеством элементов?

var things = make([]Thing, 0)


Во втором случае значение для среза ненулевое, и мы даём понять это читателю, используя короткую форму объявления:

things := make([]Thing, 0)


Это говорит читателю, что мы решили явно инициализировать things.

Так мы подходим к третьей декларации:

var thing = new(Thing)


Здесь одновременно и явная инициализация переменной, и введение «уникального» ключевого слова new, что не нравится некоторым программистам Go. Если применить рекомендованный короткий синтаксис, то получается

thing := new(Thing)


Это даёт понять, что thing явно инициализируется в результат new(Thing), но по-прежнему оставляет нетипичное new. Проблему можно было бы решить с помощью литерала:

thing := &Thing{}


Что аналогично new(Thing), а такое дублирование огорчает некоторых программистов Go. Однако это означает, что мы явно инициализируем thing с указателем на Thing{} и нулевым значением Thing.

Но лучше учесть тот факт, что thing объявляется с нулевым значением, и использовать адрес оператора для передачи адреса thing в json.Unmarshall:

var thing Thing
json.Unmarshall(reader, &thing)


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

var min int
max := 1000

Более читаемая декларация:
min, max := 0, 1000


Подведём итог:

  • При объявлении переменной без инициализации используйте синтаксис var.
  • При объявлении и явной инициализации переменной используйте :=.


Совет. Явно указывайте на сложные вещи.

var length uint32 = 0x80

Здесь length может использоваться с библиотекой, что требует определённого числового типа, и такой вариант более явно указывает, что тип length специально выбран как uint32, чем в короткой декларации:
length := uint32(0x80)

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


2.6. Работайте на коллектив


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

Изменение стилей посреди файла вызывает раздражение. Важно единообразие, пусть и в ущерб личным предпочтениям. Мое эмпирическое правило: если код подходит через gofmt, то обычно проблема не стоит обсуждения.

Совет. Если вы хотите сделать переименование по всей базе кода, не смешивайте это с другими изменениями. Если кто-то использует git bisect, ему не понравится пробираться через тысячи переименований, чтобы найти другой изменённый код.


Прежде чем мы перейдем к более важным пунктам, хочу уделить пару минут комментариям.

«У хорошего кода множество комментариев, а плохой код требует множества комментариев» — Дэйв Томас и Эндрю Хант, «Прагматичный программист»


Комментарии очень важны для читаемости программы. Каждый комментарий должен делать одну — и только одну — из трёх вещей:

  1. Объяснить, что делает код.
  2. Объяснить, как он это делает.
  3. Объяснить, почему.


Первая форма идеально подходит для комментариев к общедоступным символам:

// Open открывает указанный файл для чтения.
// В случае успеха на возвращаемом файле можно использовать методы для чтения.


Второе идеально для комментариев внутри метода:

// очередь всех зависимых действий
var results []chan error
for _, dep := range a.Deps {
        results = append(results, execute(seen, dep))
}


Третья форма («почему») уникальна тем, что она не вытесняет и не заменяет первые две. Такие комментарии объясняют внешние факторы, которые привели к написанию кода в нынешнем виде. Часто без этого контекста трудно понять, почему код написан именно таким образом.

return &v2.Cluster_CommonLbConfig{
        // Отключаем HealthyPanicThreshold
    HealthyPanicThreshold: &envoy_type.Percent{
        Value: 0,
    },
}


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

3.1. Комментарии в переменных и константах должны описывать их содержимое, а не предназначение


Ранее я говорил, что имя переменной или константы должно описывать её назначение. Но комментарий к переменной или константе должен описывать именно содержимое, а не назначение.

const randomNumber = 6 // выводится из случайной матрицы


В этом примере комментарий описывает, почему randomNumber присвоено значение 6 и откуда оно получено. Комментарий не описывает, где будет использоваться randomNumber. Вот ещё несколько примеров:

const (
    StatusContinue           = 100 // RFC 7231, 6.2.1
    StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
    StatusProcessing         = 102 // RFC 2518, 10.1

    StatusOK                 = 200 // RFC 7231, 6.3.1


В контексте HTTP число 100 известно как StatusContinue, что определено в RFC 7231, раздел 6.2.1.

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

// sizeCalculationDisabled указывает, безопасно ли
// рассчитать ширину и выравнивание типов. См. dowidth.
var sizeCalculationDisabled bool

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


Совет. Прячьте на виду. Это совет от Кейт Грегори. Иногда лучшее имя для переменной скрывается в комментариях.

// реестр драйверов SQL
var registry = make(map[string]*sql.Driver)

Комментарий добавлен автором, потому что имя registry недостаточно объясняет свое назначение — это реестр, но реестр чего?

Если переименовать переменную в sqlDrivers, то становится ясно, что она содержит драйверы SQL.

var sqlDrivers = make(map[string]*sql.Driver)

Теперь комментарий стал избыточным и его можно удалить.


3.2. Всегда документируйте общедоступные символы


Документация вашего пакета генерируется godoc, поэтому следует добавлять комментарий к каждому общедоступному символу, объявленному в пакете: переменной, константе, функции и методу.

Вот два правила из руководства по стилю Google:

  • Любая публичная функция, которая не является одновременно очевидной и краткой, должна быть прокомментирована.
  • Любая функция в библиотеке должна быть прокомментирована независимо от длины или сложности
package ioutil

// ReadAll читает из r до ошибки или конца файла (EOF) и возвращает
// прочитанные.данные. Успешный вызов возвращает err == nil, not err == EOF.
// Поскольку ReadAll должна читать до конца файла, она не интерпретирует его
// как ошибку.
func ReadAll(r io.Reader) ([]byte, error)


Из этого правила есть одно исключение: не нужно документировать методы, реализующие интерфейс. Конкретно не делайте такого:

// Read реализует интерфейс io.Reader
func (r *FileReader) Read(buf []byte) (int, error)


Этот комментарий ни о чём не говорит. Он не говорит, что делает метод: хуже того, он отправляет куда-то искать документацию. В этой ситуации я предлагаю полностью удалить комментарий.

Вот пример из пакета io.

// LimitReader возвращает Reader, который читает из r,
// но останавливается с EOF после n байт.
// Основан на *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

// LimitedReader читает из R, но ограничивает объём возвращаемых
// данных всего N байтами. Каждый вызов Read обновляет N для
// отражения новой оставшейся суммы.
// Read возвращает EOF, когда N <= 0 или когда основное R возвращает EOF.
type LimitedReader struct {
        R Reader // underlying reader
        N int64  // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
        if l.N <= 0 {
                return 0, EOF
        }
        if int64(len(p)) > l.N {
                p = p[0:l.N]
        }
        n, err = l.R.Read(p)
        l.N -= int64(n)
        return
}


Обратите внимание, что объявлению LimitedReader непосредственно предшествует функция, которая его использует, и объявление LimitedReader.Read следует за декларацией самого LimitedReader. Хотя сама LimitedReader.Read не документирована, но можно понять, что это реализация io.Reader.

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


3.2.1. Не комментируйте плохой код, перепишите его


«Не комментируйте плохой код — перепишите его» — Брайан Керниган


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

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

// TODO(dfc) является O(N^2), нужно найти более эффективную процедуру.


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

3.2.2. Вместо комментирования кода выполните его рефакторинг


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


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

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


«Пишите скромный код: модули, которые не показывают ничего лишнего другим модулям и которые не полагаются на реализации других модулей» — Дэйв Томас


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

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

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

4.1. Хороший пакет начинается с хорошего названия


Хороший пакет Go начинается с качественного названия. Представьте его как краткую презентацию, ограниченную только одним словом.

Также, как названия переменных в предыдущем разделе, имя пакета очень важно. Не надо думать о типах данных в этом пакете, лучше задать вопрос: «Какую услугу предоставляет этот пакет?» Обычно ответом будет не «Этот пакет предоставляет тип X», а «Этот пакет позволяет подключиться по HTTP».

Совет. Выбирайте название пакета по его функциональности, а не содержанию.


4.1.1. Хорошие имена пакетов должны быть уникальными


В проекте у каждого пакета уникальное название. Здесь не возникнет сложностей, если вы следовали совету давать имена по назначению пакетов. Если оказалось, что у двух пакетов одинаковые имена, скорее всего:

  1. У пакета слишком общее название.
  2. Пакет перекрывается другим пакетом с аналогичным названием. В этом случае следует либо просмотреть проект, либо рассмотреть возможность объединения пакетов.


4.2. Избегайте названий вроде base, common или util


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

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

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

»[Немного] дублирования обходится гораздо дешевле, чем неправильная абстракция» — Сэнди Мец


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

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


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

Например, для net/http не делали отдельных пакетов client и server, а вместо этого есть файлы client.go и server.go с соответствующими типами данных, а также transport.go для общего транспорта.

Совет. Важно помнить, что имя идентификатора включает название пакета.

  • Функция Get из пакета net/http становится http.Get при ссылке из другого пакета.
  • Тип Reader из пакета strings при импорте в другие пакеты превращается в strings.Reader.
  • Интерфейс Error из пакета net явно связан с сетевыми ошибками.


4.3. Быстро возвращайтесь, не погружаясь вглубь


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

Это дос

© Habrahabr.ru