Go: распространенные антипаттерны

от автора

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

Так поступают и разработчики, которые, не прекращая профессионально развиваться, постоянно стремятся найти ответ на самый важный свой вопрос: «Как писать хороший код?». Вот что говорит об этом Фредерик Брукс в книге «Мифический человеко-месяц, или Как создаются программные системы»:

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


Как писать хороший код (источник)

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

Что такое антипаттерны

Антипаттерны проникают в код тогда, когда программы пишут, не принимая во внимание вопросы, связанные с их использованием в будущем. Может случиться так, что то, что называют «анти-паттерном», в определенный момент может показаться приемлемым решением некоей задачи. Но, на самом деле, по мере роста кодовой базы подобные решения оказываются малооправданными, увеличивая технический долг проектов.

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

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

Рассмотрим некоторые распространённые антипаттерны, встречающиеся в коде, написанном на Go.

1. Возврат значения неэкспортируемого типа из экспортируемой функции

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

Имена (это относится к полям структур, к функциям, к переменным), которые начинаются с маленькой буквы, являются неэкспортируемыми, они видны только в пакете, в котором они объявлены.

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

// Не рекомендовано type unexportedType string func ExportedFunc() unexportedType {   return unexportedType("some string") }  // Рекомендовано type ExportedType string func ExportedFunc() ExportedType {      return ExportedType("some string") } 

2. Неоправданное использование пустых идентификаторов

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

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

// Не рекомендовано for _ = range sequence {     run() }  x, _ := someMap[key]  _ = <-ch  // Рекомендовано for range something {     run() }  x := someMap[key]  <-ch 

3. Использование циклов или нескольких вызовов append для объединения срезов

Если нужно объединить пару срезов — нет нужды перебирать один из них в цикле и присоединять элементы к другому срезу по одному. Вместо этого гораздо лучше и эффективнее будет сделать это в одном вызове функции append.

В следующем примере объединение срезов выполняется путём перебора элементов sliceTwo и присоединения этих элементов к sliceOne по одному:

for _, v := range sliceTwo {     sliceOne = append(sliceOne, v) } 

Но известно, что append — это вариативная функция, а это значит, что её можно вызывать с разным количеством аргументов. В результате предыдущий пример можно значительно упростить и переписать с использованием функции append:

sliceOne = append(sliceOne, sliceTwo…) 

4. Избыточные аргументы в вызовах make

В Go имеется особая встроенная функция make, которая используется для создания и инициализации объектов типов map (ассоциативный массив), slice (срез), chan (канал). Для инициализации среза с использованием make нужно предоставить этой функции, в виде аргументов, тип среза, его длину и емкость. При инициализации ассоциативного массива с помощью make нужно передать функции размер этого массива.

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

  • В случае с каналами емкость буфера устанавливается в 0 (речь идёт о небуферизованном канале).
  • В случае с ассоциативными массивами размер по умолчанию устанавливается в небольшое начальное значение.
  • В случае со срезами емкость по умолчанию устанавливается в значение, равное указанной длине среза.

Вот неудачный пример использования make:

ch = make(chan int, 0) sl = make([]int, 1, 1) 

Этот код можно переписать так:

ch = make(chan int) sl = make([]int, 1) 

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

const c = 0 ch = make(chan int, c) // Это — не антипаттерн 

5. Ненужное выражение return в функциях

Не рекомендуется ставить в конец функции выражение return в том случае, если функция ничего не возвращает.

// Бесполезное выражение return, не рекомендовано func alwaysPrintFoofoo() {     fmt.Println("foofoo")     return }  // Рекомендовано func alwaysPrintFoo() {     fmt.Println("foofoo") } 

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

func printAndReturnFoofoo() (foofoo string) {     foofoo := "foofoo"     fmt.Println(foofoo)     return } 

6. Ненужные команды break в выражениях switch

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

В результате в конце блоков case нет необходимости пользоваться командами break. Это значит, что оба нижеприведенных примера дают один и тот же результат.

// Не рекомендовано switch s { case 1:     fmt.Println("case one")     break case 2:     fmt.Println("case two") }  // Рекомендовано switch s { case 1:     fmt.Println("case one") case 2:     fmt.Println("case two") } 

Но, если нужно, в switch можно реализовать переход к последовательному выполнению кода блоков case. Для этого используется команда fallthrough. Например, следующий код выведет 23:

switch 2 { case 1:     fmt.Print("1")     fallthrough case 2:     fmt.Print("2")     fallthrough case 3:     fmt.Print("3") } 

7. Отказ от использования стандартных вспомогательных функций для решения распространённых задач

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

Например, в Go, для организации ожидания завершения выполнения нескольких горутин, можно использовать счетчик sync.WaitGroup. При работе с ним могут применяться вспомогательные функции. В частности — функция wg.Add() (переменная wg в наших примерах имеет тип sync.WaitGroup), позволяющая добавить нужное количество горутин в группу. Когда горутина из группы завершает выполнение, счетчик уменьшают, вызывая функцию wg.Add() с передачей ей -1:

wg.Add(1)  // ...какой-то код wg.Add(-1) 

Если говорить о конструкции wg.Add(-1), то, вместо того, чтобы использовать её для ручного декрементирования счетчика, можно воспользоваться функцией wg.Done(), которая тоже декрементирует счетчик, уменьшая его значение на 1, но при этом выглядит лучше и понятнее, чем wg.Add(-1):

wg.Add(1) // ... какой-то код wg.Done() 

8. Избыточные проверки на nil при работе со срезами

Длина «нулевого» (nil) среза приводится к 0. Это значит, что не нужно проверять срез на nil перед проверкой его длины.

Например, в следующем фрагменте кода проверка на nil избыточна:

if x != nil && len(x) != 0 {     // выполняем какие-то действия } 

Этот код можно переписать, убрав из него проверку на nil:

if len(x) != 0 {     // выполняем какие-то действия } 

9. Ненужные функциональные литералы

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

fn := func(x int, y int) int { return add(x, y) } 

Этот код можно улучшить, вынеся add из функционального литерала:

fn := add 

10. Использование единственного блока case в выражениях select

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

// Не рекомендовано select { case x := <-ch:     fmt.Println(x) }  // Рекомендовано x := <-ch fmt.Println(x) 

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

select { case x := <-ch:     fmt.Println(x) default:     fmt.Println("default") } 

11. Параметр типа context.Context, который не является первым параметром функции, в которой используется этот параметр

Если функция имеет параметр типа context.Context, то ему обычно дают имя ctx, а при объявлении функции его следует ставить первым в списке параметров. Такой аргумент используется в Go-функциях достаточно часто, а подобные аргументы, с логической точки зрения, лучше размещать в начале или в конце списка аргументов. Почему?

Это помогает разработчикам не забывать об этих аргументах благодаря единообразному подходу к их использованию в различных функциях. Вариативные функции в Go объявляют с использованием конструкции вида elems ...Type, которая должна располагаться в конце списка их параметров. В результате рекомендуется делать параметр типа context.Context первым параметром функции. Подобные соглашения имеются и в других проектах, например, в среде Node.js первым параметром, который передают коллбэкам, принято делать объект ошибки.

// Не рекомендовано func badPatternFunc(k favContextKey, ctx context.Context) {         // выполняем какие-то действия }  // Рекомендовано func goodPatternFunc(ctx context.Context, k favContextKey) {         // выполняем какие-то действия } 

На что стоит обратить внимание тем, кто хочет писать хороший код на Go?

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/551032/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *