📕
Golang
  • Собеседование по golang
  • Вопросы собеседования
    • Список вопросов МТС
    • Список вопросов
    • Базовые вопросы по Golang
    • Go. Прорабатываем 25 основных вопросов собеседования
    • Функции
    • Структуры данных
    • Конкурентность и Параллелизм
    • Горутины
    • Примитивы синхронизации
    • Планировщик
    • Go: конкурентность и привязки к потокам в планировщике
    • Каналы
    • GC
  • спецификация
    • Спецификация Go: преобразования в и из строкового типа
    • переключатель типов (type switch)
    • for утверждения (for statements)
    • for утверждения с range условием
    • go утверждения (go statements)
    • select утверждения (select statements)
    • return утверждения (return statements)
    • continue утверждения (continue statements)
    • goto утверждения (goto statements)
    • fallthrough утверждения (fallthrough statements, утверждения "провала")
    • defer утверждения (defer statements)
    • встроенные функции, функция close
    • длина и емкость
    • аллокация, создание срезов (slice), карт (map) и каналов
    • добавление в срезы и копирование срезов
    • удаление элементов карты
    • обработка паники
    • начальная загрузка (bootstrapping)
    • пакеты
    • инициализация и выполнение программы, нулевое значение
    • инициализация пакета
    • выполнение программы
    • ошибки
    • паника во время выполнения (run-time panic)
  • Эффективный go
    • эффективный go
    • 50 оттенков go
    • Go: распространенные антипаттерны
    • Визуализация concurrency в Go с WebGL
  • требования для работы
    • Список навыков
  • habr
    • Изучаем многопоточное программирование в Go по картинкам
    • Go: конкурентность и привязки к потокам в планировщике
  • NP
    • Полиморфизм с интерфейсами в Golang
    • Объектно-ориентированное программирование в Golang
    • Владеешь merge  -  освой и rebase
  • ProgLib
    • Горутины
  • Untitled
  • Оптимизация
    • Go: Должен ли я использовать указатель вместо копии моей структуры?
  • Полезняшки
    • Using PostgreSQL JSONB with Go
Powered by GitBook
On this page
  • Что такое антипаттерны
  • 1. Возврат значения неэкспортируемого типа из экспортируемой функции
  • 2. Неоправданное использование пустых идентификаторов
  • 3. Использование циклов или нескольких вызовов append для объединения срезов
  • 4. Избыточные аргументы в вызовах make
  • 5. Ненужное выражение return в функциях
  • 6. Ненужные команды break в выражениях switch
  • 7. Отказ от использования стандартных вспомогательных функций для решения распространённых задач
  • 8. Избыточные проверки на nil при работе со срезами
  • 9. Ненужные функциональные литералы
  • 10. Использование единственного блока case в выражениях select
  • 11. Параметр типа context.Context, который не является первым параметром функции, в которой используется этот параметр

Was this helpful?

  1. Эффективный go

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

Previous50 оттенков goNextВизуализация concurrency в Go с WebGL

Last updated 4 years ago

Was this helpful?

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

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

Антипаттерны проникают в код тогда, когда программы пишут, не принимая во внимание вопросы, связанные с их использованием в будущем. Может случиться так, что то, что называют «анти-паттерном», в определенный момент может показаться приемлемым решением некоей задачи. Но, на самом деле, по мере роста кодовой базы подобные решения оказываются малооправданными, увеличивая технический долг проектов. В качестве простого примера проникновения в код антипаттерна можно привести ситуацию, когда при создании 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/
источник