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

Kate

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

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

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



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

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

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


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

В качестве простого примера проникновения в код антипаттерна можно привести ситуацию, когда при создании 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) {
// выполняем какие-то действия
}


Источник статьи: https://habr.com/ru/company/ruvds/blog/551032/
 
Сверху