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

Программирование — это искусство. Мастера своего дела, создающие потрясающие работы, могут ими гордиться. То же самое относится и к программистам, которые пишут код. Чтобы достичь вершин мастерства, творцы постоянно ищут новые подходы к работе и новые инструменты.\
\
&#x20;Так поступают и разработчики, которые, не прекращая профессионально развиваться, постоянно стремятся найти ответ на самый важный свой вопрос: «Как писать хороший код?». Вот что говорит об этом Фредерик Брукс в книге «Мифический человеко-месяц, или Как создаются программные системы»:\
\
&#x20;Программист, подобно поэту, работает с чистой мыслью. Он строит свои замки в воздухе и из воздуха, творя силой воображения. Трудно найти другой материал, используемый в творчестве, который столь же гибок, прост для шлифовки или переработки и доступен для воплощения грандиозных замыслов.\
\
[![](https://habrastorage.org/getpro/habr/post_images/dc3/c6e/c42/dc3c6ec4259cf7ca7d6973d971d71dfd.png)](https://habr.com/ru/company/ruvds/blog/551032/)\
&#x20;Как писать хороший код ([источник](http://comicsia.ru/collections/xkcd/i33281.html))\
\
&#x20;В этом материале сделана попытка найти ответ на большой вопрос из вышеприведенного комикса. Самый простой способ писать хороший код заключается в том, чтобы не употреблять в своих программах так называемые «антипаттерны».\ <br>

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

\
&#x20;Антипаттерны проникают в код тогда, когда программы пишут, не принимая во внимание вопросы, связанные с их использованием в будущем. Может случиться так, что то, что называют «анти-паттерном», в определенный момент может показаться приемлемым решением некоей задачи. Но, на самом деле, по мере роста кодовой базы подобные решения оказываются малооправданными, увеличивая технический долг проектов.\
\
&#x20;В качестве простого примера проникновения в код антипаттерна можно привести ситуацию, когда при создании API не учитывается то, как именно потребитель этого API будет им пользоваться. Этому посвящён наш первый рассказ об антипаттерне.\
\
&#x20;Знание антипаттернов и сознательное предотвращение их использования в ходе написания кода — это, без сомнения, чрезвычайно важный шаг на пути к улучшению читабельности и поддерживаемости кодовой базы.\
\
&#x20;Рассмотрим некоторые распространённые антипаттерны, встречающиеся в коде, написанном на Go.\ <br>

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

\
&#x20;В Go, для экспорта любого поля или любой переменной, нужно, чтобы имя поля или переменной начиналось бы с большой буквы. Сущности экспортируют из пакетов для того чтобы они были бы видны другим пакетам. Например, если в программе нужно воспользоваться константой `Pi` из пакета `math`, то обращаться к ней надо с помощью конструкции `math.Pi`. Использование конструкции `math.pi` приведет к возникновению ошибки.\
\
&#x20;Имена (это относится к полям структур, к функциям, к переменным), которые начинаются с маленькой буквы, являются неэкспортируемыми, они видны только в пакете, в котором они объявлены.\
\
&#x20;Если экспортируемая функция возвращает значение неэкспортируемого типа — это может привести к неудобствам, так как тому, кто вызывает эту функцию из другого пакета, придётся самостоятельно определить тип этого значения для его использования.\ <br>

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

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

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

\
&#x20;В целом ряде ситуаций присвоение значений пустому идентификатору нецелесообразно. Вот, например, что сказано в спецификации Go об использовании пустого идентификатора в циклах `for`:\
\
&#x20;Если последней итерационной переменной является пустой идентификатор, то выражение range эквивалентно такому же выражению без этого идентификатора.\ <br>

```
// Не рекомендовано
for _ = range sequence
{
    run()
}

x, _ := someMap[key]

_ = <-ch

// Рекомендовано
for range something
{
    run()
}

x := someMap[key]

<-ch
```

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

\
&#x20;Если нужно объединить пару срезов — нет нужды перебирать один из них в цикле и присоединять элементы к другому срезу по одному. Вместо этого гораздо лучше и эффективнее будет сделать это в одном вызове функции `append`.\
\
&#x20;В следующем примере объединение срезов выполняется путём перебора элементов `sliceTwo` и присоединения этих элементов к `sliceOne` по одному:\ <br>

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

\
&#x20;Но известно, что `append` — это вариативная функция, а это значит, что её можно вызывать с разным количеством аргументов. В результате предыдущий пример можно значительно упростить и переписать с использованием функции `append`:\ <br>

```
sliceOne = append(sliceOne, sliceTwo…)
```

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

\
&#x20;В Go имеется особая встроенная функция `make`, которая используется для создания и инициализации объектов типов `map` (ассоциативный массив), `slice` (срез), `chan` (канал). Для инициализации среза с использованием `make` нужно предоставить этой функции, в виде аргументов, тип среза, его длину и емкость. При инициализации ассоциативного массива с помощью `make` нужно передать функции размер этого массива.\
\
&#x20;Правда, пользуясь `make`, нужно знать о том, что у этой функции уже имеются значения, назначаемые соответствующим аргументам по умолчанию:\ <br>

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

\
&#x20;Вот неудачный пример использования `make`:\ <br>

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

\
&#x20;Этот код можно переписать так:\ <br>

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

\
&#x20;Надо отметить, что использование именованных констант при создании каналов не считается анти-паттерном в тех случаях, когда речь идёт об отладке, о применении результатов неких вычислений, о написании кода, жёстко привязанного к какой-либо платформе.\ <br>

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

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

\
&#x20;Не рекомендуется ставить в конец функции выражение `return` в том случае, если функция ничего не возвращает.\ <br>

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

// Рекомендовано
func alwaysPrintFoo() {
    fmt.Println("foofoo")
}
```

\
&#x20;При этом надо отметить, что возврат с помощью `return` именованных возвращаемых значений не стоит путать с бесполезным использованием `return`. Например, в следующем фрагменте кода `return` возвращает именованное значение:\ <br>

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

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

\
&#x20;В Go выражения `switch` устроены так, что при выполнении одного из вариантов кода, описываемого в блоке `case`, код блоков `case`, которые следуют за ним, выполняться не будет. В других языках, наподобие C, выполнение кода должно быть явным образом прервано с помощью команды `break`. В противном случае, если, например, в `switch` нет ни одного `break`, после выполнения кода одного блока `case` выполняется и код следующих за ним блоков. Известно, что эта возможность в выражениях `switch` используется очень редко и обычно вызывает ошибки. В результате многие современные языки программирования, вроде Go, отказались от такой схемы выполнения выражений `switch`.\
\
&#x20;В результате в конце блоков `case` нет необходимости пользоваться командами `break`. Это значит, что оба нижеприведенных примера дают один и тот же результат.\ <br>

```
// Не рекомендовано
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")
}
```

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

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

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

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

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

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

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

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

\
&#x20;Длина «нулевого» (`nil`) среза приводится к 0. Это значит, что не нужно проверять срез на `nil` перед проверкой его длины.\
\
&#x20;Например, в следующем фрагменте кода проверка на `nil` избыточна:\ <br>

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

\
&#x20;Этот код можно переписать, убрав из него проверку на `nil`:\ <br>

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

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

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

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

\
&#x20;Этот код можно улучшить, вынеся `add` из функционального литерала:\ <br>

```
fn := add
```

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

\
&#x20;Выражения `select` используются при работе с каналами. Обычно они включают в себя несколько блоков `case`. Но в том случае, если речь идёт об обработке единственной операции, представленной единственным блоком `case`, использование выражения `select` оказывается избыточным. В подобной ситуации можно просто воспользоваться операциями отправки данных в канал или их получения из канала:\ <br>

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

// Рекомендовано
x := <-ch
fmt.Println(x)
```

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

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

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

\
&#x20;Если функция имеет параметр типа `context.Context`, то ему обычно дают имя `ctx`, а при объявлении функции его следует ставить первым в списке параметров. Такой аргумент используется в Go-функциях достаточно часто, а подобные аргументы, с логической точки зрения, лучше размещать в начале или в конце списка аргументов. Почему?\
\
&#x20;Это помогает разработчикам не забывать об этих аргументах благодаря единообразному подходу к их использованию в различных функциях. Вариативные функции в Go объявляют с использованием конструкции вида `elems ...Type`, которая должна располагаться в конце списка их параметров. В результате рекомендуется делать параметр типа `context.Context` первым параметром функции. Подобные соглашения имеются и в других проектах, например, в среде Node.js первым параметром, который передают коллбэкам, принято делать объект ошибки.\ <br>

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

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

Ссылки:

<https://habr.com/ru/company/ruvds/blog/551032/>
