Библиотека Go для собеса | вопросы с собеседований
6.88K subscribers
223 photos
7 videos
1 file
432 links
Вопросы с собеседований по Go и ответы на них.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/0b524a15

Для обратной связи: @proglibrary_feeedback_bot

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
💬 Если ключ или значение типа map имеют размер более 128 байт, каким образом Go их будет хранить?

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

📌Хоть все происходит под капотом, это может значительно повлиять на производительность и управление памятью.

👉 Читайте подробнее об утечках памяти при работе с мапами
👍9🔥1
💬Как использовать операторы == и != для эффективного сравнения значений в Go?

📌Мы можем использовать эти операторы с операндами, которые сравнимы:

Логические: равны ли два логических значения.
Числовые (int, float, complex): равны ли два числовых значения.
Строки: равны ли две строки.
Каналы: созданы ли два канала одним вызовом make или оба равны nil.
Интерфейсы: имеют ли два интерфейса идентичные динамические типы и равные динамические значения или оба равны nil.
Указатели: указывают ли два указателя на одно и то же значение в памяти или оба равны nil.
Структуры и массивы: состоят ли они из аналогичных типов.

📌Также мы можем использовать операторы <=, >=, < и > с числовыми типами для сравнения значений и со строками для сравнения их лексического порядка. Если операнды несравнимы, мы должны использовать другие варианты, такие как рефлексия.

📌Например, в Go мы можем использовать reflect.DeepEqual. Эта функция сообщает, равны ли два элемента, рекурсивно обходя два значения. Элементы, которые она принимает, это базовые типы, массивы, структуры, срезы, мапы, указатели, интерфейсы и функции. Однако основной недостаток — это производительность.

📌Важно помнить, что в стандартной библиотеке есть некоторые существующие методы сравнения, такие как bytes.Compare, slices.Compare и другие.
5👍2
💬Как эффективно инициализировать тип map в Go?

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

🔸Если мы заранее знаем количество элементов, которые будет содержать мапа, эффективнее будет создать ее, указав начальный размер. Это позволяет избежать потенциального расширения мапы, что довольно сложно с точки зрения вычислений, поскольку требует перераспределения достаточного пространства памяти и перебалансировки всех элементов.
7👍3🥱1
💬Какие подводные камни необходимо учитывать при работе с числами с плавающей точкой в Go?

📌В Go существует два типа чисел с плавающей точкой (если не учитывать комплексные числа): float32 и float64. Концепция числа с плавающей точкой была изобретена для решения основной проблемы целых чисел: их неспособности представлять дробные значения.

📌Чтобы избежать неприятных сюрпризов, нам нужно помнить, что арифметика с плавающей точкой является приближением к реальной арифметике.

📌Для примера посмотрим на умножение:

var n float32 = 1.0001
fmt.Println(n * n)


◾️Мы могли бы ожидать, что этот код выведет результат умножения 1.0001 * 1.0001 = 1.00020001. Однако, если запустить его на большинстве процессоров x86, он выведет 1.0002.

◾️Поскольку типы float32 и float64 в Go являются приближениями, нам нужно помнить несколько правил:

* При сравнении двух чисел с плавающей точкой проверяйте, что их разница находится в приемлемом диапазоне.
* При выполнении сложений или вычитаний группируйте операции с похожим порядком величины для большей точности.
* Чтобы повысить точность, если последовательность операций требует сложения, вычитания, умножения или деления, сначала выполняйте операции умножения и деления.
👍9
💬Что важно учитывать при работе с циклом range в Go?

📌 Value в цикле range является копией. Следовательно, чтобы изменить структуру, необходимо обращаться к ней через индекс или использовать классический цикл for (если только элемент или поле, которое мы хотим модифицировать, не является указателем).

📌 Цикл range оценивает предоставленное выражение только один раз, до начала цикла, путем создания копии (независимо от типа). Важно помнить об этом поведении, чтобы избежать распространенных ошибок, которые, например, могут привести к доступу к неправильному элементу.

Например:


a := [3]int{0, 1, 2}
for i, v := range a {
a[2] = 10
if i == 2 {
fmt.Println(v)
}
}


Массив a инициализируется значениями [0, 1, 2], при этом изменение a[2] на 10 не влияет на итерацию, так как массив был оценен до начала цикла. Поэтому, когда индекс i равен 2, переменная v (которая является копией элемента массива на момент начала цикла) все еще содержит исходное значение 2, а не обновленное значение 10.
👍14
💬Как реализовать тайм-ауты для каналов в Go?

🔸Применение select для мультиплексирования каналов открывает широкие возможности и помогает сделать сложные или утомительные задачи тривиально простыми.

🔸В некоторых языках для реализации тайм-аута может потребоваться реализовать управление потоками, но select с вызовом функции time.After, возвращающей канал, через который будет отправлено сообщение после истечения указанного времени, делает эту задачу очень простой:


var ch chan int
select {
case m := <-ch:
fmt.Println(m)
case <-time.After(10 * time.Second):
fmt.Println("Timed out")
}

🔸Здесь нет оператора default, поэтому select заблокируется до выполнения одного из условий. Если канал ch не станет доступным для чтения до того, как в канал, возвращаемый функцией time.After, будет записано сообщение, то сработает второй оператор case и инструкция select
з
авершится по тайм-ауту.
🔥132
🧑‍💻 Статьи для IT: как объяснять и распространять значимые идеи

Напоминаем, что у нас есть бесплатный курс для всех, кто хочет научиться интересно писать — о программировании и в целом.

Что: семь модулей, посвященных написанию, редактированию, иллюстрированию и распространению публикаций.

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

👉Материалы регулярно дополняются, обновляются и корректируются. А еще мы отвечаем на все учебные вопросы в комментариях курса.
💬 В чем преимущества и недостатки импорта через точку в Go?

В Go, импорт пакета с использованием точки (dot import) является специальной формой импорта, позволяющей обращаться к экспортируемым идентификаторам пакета непосредственно, без указания имени пакета.

📌 Обычный импорт:

import "fmt"

func main() {
fmt.Println("Hello, World!")
}


Здесь Println вызывается с использованием имени пакета fmt.

📌 Импорт через точку:

import . "fmt"

func main() {
Println("Hello, World!") // Используется без указания пакета
}

Здесь Println вызывается напрямую, без упоминания fmt.

📌 Преимущества и недостатки:

1. Удобство: импорт через точку может сделать код более лаконичным, особенно если из пакета часто используются различные функции или типы.

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

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

💡Такой подход загрязняет пространство имен текущего пакета. Каждая функция или тип, которые мы импортируем через точку, удаляет возможность записи локальной функции или типа с тем же именем.
👍8
💬 Что из себя представляет тип any в Go?

В Go, any — это псевдоним для интерфейса interface{}, который по сути может представлять любой тип данных. Это удобно, когда мы не знаем заранее, какой тип данных будет использоваться.

☑️ any может хранить значение любого типа, от примитивов до сложных кастомных структур. Это делает его идеальным для случаев, когда тип данных заранее неизвестен.

☑️ С введением дженериков в Go 1.18, any стал широко использоваться для создания обобщенных функций и типов. Он позволяет определять параметры и структуры, которые могут работать с любым типом данных.

☑️ Поскольку any может представлять любой тип, он полезен в ситуациях, где нужна гибкость типов данных, например, при работе с JSON или при динамическом приведении типов.

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

📌 Функция, принимающая любой тип:

func PrintValue(v any) {
fmt.Println(v)
}

func main() {
PrintValue(5) // 5
PrintValue("hello") // hello
PrintValue(3.14) // 3.14
}


📌 Хранение различных типов в срезе:

func main() {
values := []any{5, "hello", 3.14}
for _, v := range values {
fmt.Println(v)
}
}
👍112
💬 Что такое pprof и как его использовать в Go?

📌 pprof — это инструмент для визуализации и анализа профилей производительности, встроенный в экосистему Go. Он помогает в обнаружении узких мест в коде и понимании того, как программа использует ресурсы, такие как CPU и память.

📌 Использование pprof на практике:

1. Импорт пакета pprof: испортируем пакет import _ "net/http/pprof", что позволит автоматически добавить обработчики профайлера к HTTP-серверу.

2. Запуск HTTP сервера: pprof использует HTTP-сервер для сбора и предоставления данных профиля.

Мы можем запустить сервер на определенном порту, используя:

go
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()


Это позволит получить доступ к профилям через http://localhost:6060/debug/pprof/.

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

go
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()


Этот код запускает сбор данных о производительности CPU.

4. Анализ профиля: после сбора данных профиля мы можем использовать инструмент командной строки go tool pprof для анализа собранных данных. Например:

shell
go tool pprof cpu.prof


В интерактивном режиме pprof мы можем визуализировать данные, искать узкие места, и просматривать статистику вызовов.

5. Визуализация: pprof также поддерживает визуализацию данных профиля в виде графиков. Мы можем использовать команды внутри pprof, такие как web или svg, для создания графического представления профиля.
👍121
💬 Какие пакеты Go используются для работы с DNS и разрешением доменных имен?

📌 Для работы с DNS и выполнения разрешения доменных имен в Go можно использовать библиотеку net. Например:

🔸net.LookupHost: преобразовывает имя домена в список соответствующих IP-адресов.

func main() {
ips, err := net.LookupHost("example.com")
if err != nil {
fmt.Println("Error:", err)
return
}

for _, ip := range ips {
fmt.Println(ip)
}
}


🔸net.LookupMX: получает mx-запись для указанного домена, что полезно при разработке почтовых систем.


func main() {
mxRecords, err := net.LookupMX("example.com")
if err != nil {
fmt.Println("Error:", err)
return
}

for _, mx := range mxRecords {
fmt.Println(mx.Host, mx.Pref)
}
}
👍14👏2
💬 В каких случаях следует использовать интерфейсы в Go?

🤔 Это довольно обширная и сложная тема, но если отвечать кратко, то интерфейсы в Go следует создавать в нескольких сценариях:

1️⃣ Общее поведение — использование интерфейсов, когда несколько типов реализуют общее поведение. Тогда можно заключить это поведение внутрь какого-то интерфейса.

В стандартной библиотеке много таких примеров. Например, сортировка какой-либо коллекции может быть разложена на три действия:

✹ Получение данных о количестве элементов в коллекции.
✹ Сообщение о том, должен ли один элемент быть размещен перед другим.
✹ Перестановка двух элементов.

2️⃣ Снижение связанности (decoupling) — отделение кода от его реализации. Если мы полагаемся на абстракцию вместо конкретной реализации, сама реализация может быть заменена на другую без необходимости менять код. Это и есть принцип подстановки Лисков. Одно из преимуществ снижения связанности может относиться к юнит-тестам.

3️⃣ Ограничение поведения. Представим, что мы реализуем конфигурационный пакет для работы с динамической конфигурацией. Мы создаем специальный контейнер для конфигураций int с помощью структуры IntConfig, в которой определены два метода: Get и Set. Вот как будет выглядеть такой код:

type IntConfig struct {
// ...
}
func (c *IntConfig) Get() int {
// Получить конфигурацию
}
func (c *IntConfig) Set(value int) {
// Обновить конфигурацию
}


✹ Теперь предположим, что мы получили IntConfig, который содержит в себе определенную конфигурацию, например какое-то пороговое значение. Но в нашем коде нас интересует только получение значения этой конфигурации, и мы хотим предотвратить его обновление. Как мы можем обеспечить, чтобы семантически эта конфигурация была доступна только для чтения, если мы не хотим изменять пакет конфигурации?

✹ Cоздав абстракцию, которая ограничивает поведение только получением значения конфигурации:

type intConfigGetter interface {
Get() int
}


Тогда в коде можно указать только intConfigGetter вместо конкретной реализации:


type Foo struct {
threshold intConfigGetter
}

func NewFoo(threshold intConfigGetter) Foo {
return Foo{threshold: threshold}
}

func (f Foo) Bar() {
threshold := f.threshold.Get()
// ...
}


✹ В примере геттер конфигурации внедряется в фабричный метод NewFoo. Он не влияет на потребителя этой функции, поскольку он по-прежнему может передавать структуру IntConfig по мере реализации intConfigGetter. Затем в методе Bar можно только прочитать конфигурацию, но не изменить ее. Поэтому мы также можем использовать интерфейсы, чтобы ограничить тип определенным поведением, например, если нужно соблюсти семантику.
👍9
💬 Как обнаружить целочисленное переполнение при инкрементировании в Go?

📌 Чтобы обнаружить целочисленное переполнение при выполнении операции инкрементального увеличения значения переменной типа, основанного на определенном размере (int8, int16, int32, int64, uint8, uint16, uint32 или uint64), можно сравнивать это значение с математическими константами.

🔸 Например, в случае с int32:

func Inc32(counter int32) int32 {
if counter == math.MaxInt32 {
panic("int32 overflow")
}
return counter + 1
}



🔸 Эта функция проверяет, достигла ли переменная значения math.MaxInt32. Если да, то ее увеличение приведет к переполнению.

🔸 А что насчет типов int и uint?

До версии Go 1.17 приходилось создавать эти константы вручную. Теперь же math.MaxInt, math.MinInt и math.MaxUint стали частью пакета math. Если нужно проверить на переполнение переменную типа int, можно сделать это с помощью math.MaxInt:

func IncInt(counter int) int {
if counter == math.MaxInt {
panic("int overflow")
}
return counter + 1
}



Логика та же самая для uint. Можно использовать math.MaxUint:

func IncUint(counter uint) uint {
if counter == math.MaxUint {
panic("uint overflow")
}
return counter + 1
}
👍11
💬 Какие могут быть побочные эффекты от именованных параметров результата функции?

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

🤔 Что не так с этим кодом?

func (l loc) getCoordinates(ctx context.Context, address string) (
lat, lng float32, err error) {
isValid := l.validateAddress(address)
if !isValid {
return 0, 0, errors.New("invalid address")}
if ctx.Err() != nil {
return 0, 0, err
}
}


◆ На первый взгляд ошибка может быть неочевидной. Ошибка, возвращаемая в области видимости if ctx.Err() != nil, — это err. Но мы не присвоили переменной err никакого значения. Ей по-прежнему присвоено nil. Следовательно, этот код всегда будет возвращать ошибку nil.

◆ Код скомпилируется, потому что err была инициализирована нулевым значением благодаря именованным параметрам результата. Без присвоения имени мы получили бы ошибку компиляции:

Unresolved reference 'err'


◆ Один из возможных выходов — сделать переменную err равной ctx.Err():

if err := ctx.Err(); err != nil {
return 0, 0, err
}


◆ Мы продолжаем возвращать err, но сначала присваиваем ей результат ctx.Err(). Обратите внимание, что err в этом примере затеняет переменную результата.

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

◆ Другой вариант — использовать пустой оператор return:

if err = ctx.Err(); err != nil {
return
}


◆ Но при этом нарушается правило о том, что не нужно смешивать в одном фрагменте кода пустые операторы return с такими же операторами, но с аргументами.

💡 Применение именованных параметров результата не всегда равно требованию применять пустые операторы return. Иногда можно просто использовать именованные параметры результата, чтобы сделать сигнатуру функции более чистой.
👍7
💬 Какие подводные камни существуют при создании копии срезов в Go?

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

🔸В следующем примере мы создаем один срез, копируем его элементы в другой и получаем [] вместо [0 1 2]:

src := []int{0, 1, 2}
var dst []int
copy(dst, src)
fmt.Println(dst)


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

🔸В предыдущем примере src — это срез длиной 3, а dst — срез с нулевой длиной, поскольку он инициализируется со своим нулевым значением. Поэтому функция copy копирует количество элементов, равное минимуму в наборе 3 и 0: здесь этот минимум будет равен 0. Поэтому полученный срез будет пустым.

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

src := []int{0, 1, 2}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst)


🔸Поскольку dst теперь срез, инициализированный с длиной, равной 3, то копируются три элемента. На этот раз результатом будет [012].

💡Другая распространенная ошибка — инвертировать порядок аргументов при вызове функции copy. Помните, что срез, в который происходит копирование, — первый аргумент, а срез-источник — второй.

🔹Использование встроенной функции copy — не единственный способ копирования элементов среза. Есть другие альтернативы:

src := []int{0, 1, 2}
dst := append([]int(nil), src...)


🔹Мы добавляем элементы из исходного среза в другой, нулевой. Следовательно, этот код создает копию среза длиной 3 и емкостью 3. Однако использование функции copy более идиоматично и, следовательно, легче для понимания, даже несмотря на то, что требует больше кода.
👍111
💬 Какие подводные камни могут возникнуть при работе с циклом range в Go?

📌 Синтаксис цикла range требует наличия выражения. Например, в цикле for i, v := range exp, exp — это выражение. Это может быть строка, массив, указатель на массив, срез, map или канал.

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

🤔 Завершится ли этот цикл?

s := []int{0, 1, 2}
for range s {
s = append(s, 10)
}


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

◆ В этом контексте слово «вычисляется» означает, что предоставленное выражение копируется во временную переменную, а затем цикл range выполняет итерации. В этом примере при вычислении выражения s результатом будет копия среза. Цикл range использует эту временную переменную. Исходный срез s также обновляется во время каждой итерации.

◆ Каждый шаг приводит к добавлению нового элемента. Но за три шага мы прошлись по всем его элементам. Длина временного среза, используемого в range, остается равна 3, поэтому цикл завершается после трех итераций.

◆ Такое поведение отличается от классического цикла for:

s := []int{0, 1, 2}
for i := 0; i < len(s); i++ {
s = append(s, 10)
}


◆ В этом примере цикл никогда не закончится. Значение выражения len(s) вычисляется во время каждой итерации, и раз мы продолжаем добавлять элементы, то никогда не достигнем состояния завершения цикла.

💡 Чтобы правильно использовать циклы в Go, важно помнить об этой разнице. При использовании range помните, что вышеописанное поведение (выражение вычисляется только один раз) также применимо ко всем типам данных.
🔥26👍54
💬 Что такое deadline в контексте Go context?

🔸deadline указывает на момент времени, определяемый одним из следующих способов:

time.Duration с настоящего момента (например, через 250 мс);
time.Time (например, 2023-02-07 00:00:00 UTC).

🔸Семантика deadline означает, что текущая деятельность должна быть остановлена, если этот крайний срок наступил. «Деятельность» — это, например, запрос типа ввод/вывод или горутина в состоянии ожидания получения сообщения из канала.

🔸Рассмотрим пример, который каждые 4 секунды получает от радара данные о позиции самолета. Получив позицию, мы хотим поделиться ею с другими приложениями, для которых интерес представляет только последняя позиция.

🔸В нашем распоряжении есть интерфейс publisher, содержащий в себе одинединственный метод:

type publisher interface {
Publish(ctx context.Context, position flight.Position) error
}


🔸Этот метод принимает контекст и позицию. Предполагается, что конкретная реализация вызывает функцию для публикации сообщения брокеру (например, использование Sarama для публикации сообщения Kafka).

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

🔸Создаваемый контекст должен сообщать о ней через 4 секунды, а если мы не смогли опубликовать позицию, то следует остановить вызов Publish:

type publishHandler struct {
pub publisher
}
func (h publishHandler) publishPosition(position flight.Position) error {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
return h.pub.Publish(ctx, position)
}


🔸Код создает контекст с помощью функции context.WithTimeout, которая принимает тайм-аут и контекст. Поскольку publishPosition не получает существующий контекст, мы создаем его из пустого контекста с помощью context.Background. Между тем context.WithTimeout возвращает две переменные: созданный контекст и функцию отмены func(), которая отменит контекст после вызова. Передача созданного контекста в метод Publish должна произойти не позднее чем через 4 секунды.

🔸В чем смысл вызова функции cancel как функции defer? WithTimeout создает горутину, которая будет храниться в памяти в течение 4 секунд или до тех пор, пока не будет вызвана cancel. Следовательно, вызов cancel в качестве функции defer означает, что при выходе из родительской функции контекст будет отменен, а созданная горутина остановлена. Это мера предосторожности, чтобы при возвращении мы не оставили в памяти сохраненные объекты.
👍12
💬 В Go существует несколько способов возврата структур или их частей. Назовите основные.

1. Возврат копии структуры:

type MyStruct struct { Value int }

func returnCopy() MyStruct {
return MyStruct{Value: 1}
}


Здесь функция returnCopy возвращает копию структуры MyStruct. Изменения возвращаемой копии не затронут оригинал.

2. Возврат указателя на структуру:

func returnPointer() *MyStruct {
return &MyStruct{Value: 2}
}


В этом случае функция returnPointer возвращает указатель на структуру MyStruct. Это позволяет избежать копирования и работать непосредственно с объектом.

3. Изменение структуры, переданной по указателю:

func modifyStruct(s *MyStruct) {
s.Value = 3
}


Функция modifyStruct ожидает указатель на структуру и изменяет её напрямую. Это позволяет функции влиять на исходный объект.

4. Возврат части структуры:

func returnValue(s MyStruct) int {
return s.Value
}


Здесь функция returnValue возвращает только значение поля Value из структуры.

5. Возврат через интерфейс:
Предположим, у нас есть интерфейс MyInterface, и структура MyStruct его реализует.

type MyInterface interface { DoSomething() }
type MyStruct struct { /* ... */ }

func (m MyStruct) DoSomething() { /* ... */ }

func returnInterface() MyInterface {
return MyStruct{}
}


Функция returnInterface возвращает экземпляр MyStruct, но тип возврата — интерфейс MyInterface.

6. Использование срезов и мап структур:

func returnSlice() []MyStruct {
return []MyStruct{{Value: 4}, {Value: 5}}
}

func returnMap() map[string]MyStruct {
return map[string]MyStruct{"first": {Value: 6}, "second": {Value: 7}}
}


7. Возврат структуры через канал:

func returnThroughChannel(ch chan MyStruct) {
ch <- MyStruct{Value: 8}
}


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

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

👉 Вдохновлено вопросом на StackOverflow
👍12
💬 Что из себя представляет паттерн Retry в Go?

Паттерн Retry (Повтор) учитывает возможный временный характер ошибки в распределенной системе и осуществляет повторные попытки выполнить неудачную операцию.

Любое взаимодействие программных компонентов ненадежно. Вызываемый компонент может быть временно недоступен или возвращать различные ошибки. Особенно если взаимодействие происходит по сети.

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

📌 Компоненты

🔸Effector — функция, взаимодействующая с сервисом.
🔸 Retry — функция, принимающая Effector.

📌Пример кода

Сигнатура функции Effector:

type Effector func(context.Context) (string, error)


Функция Retry:

func Retry(effector Effector, retries int, delay time.Duration) Effector { 
return func(ctx context.Context) (string, error) {

for r := 0; ; r++ {
response, err := effector(ctx)
if err == nil || r >= retries {
return response, err
}

log.Printf("Attempt %d failed; retrying in %v", r + 1, delay)
select {
case <-time.After(delay):
case <-ctx.Done():
return "", ctx.Err()
}
}
}
}


Возможно, вы сами заметили, почему функция Retry имеет такую стройную реализацию: она возвращает функцию, но у этой функции нет внешнего состояния.

Чтобы использовать Retry, необходимо реализовать функцию, которая выполняет потенциально неудачную операцию, чья сигнатура соответствует типу Effector. Выберем на эту роль функцию EmulateTransientError:

var count int

func EmulateTransientError(ctx context.Context) (string, error) {
count++

if count <= 3 {
return "intentional fail", errors.New("error")
} else {
return "success", nil
}
}

func main() {
r := Retry(EmulateTransientError, 5, 2*time.Second)
res, err := r(context.Background())
fmt.Println(res, err)
}


Функция main передает функцию EmulateTransientError в Retry и сохраняет полученную функцию в переменной r. Когда будет вызвана функция r, она вызовет EmulateTransientError и повторит попытку после задержки, если EmulateTransientError вернет ошибку, в соответствии с логикой повтора, показанной выше. Наконец, после четвертой попытки EmulateTransientError возвращает nil в качестве ошибки и завершает работу.
👍181
💬 Как выглядит сокращенная форма объявления переменных в Go?

🔸 Go поддерживает синтаксический сахар, позволяющий одновременно объявлять переменные и присваивать им значения внутри функций: оператор ":=" вместо объявления var с неявным типом.

🔸 В общем случае сокращенная форма объявления имеет вид:

имя := выражение


🔸 С его помощью можно объявить и одну, и сразу несколько переменных: 

◆ С инициализацией: percent := rand.Float64() * 100.0
◆ Сразу несколько переменных: x, y := 0, 2

🔸 На практике сокращенная форма является наиболее распространенным способом объявления и инициализации переменных; ключевое слово var обычно используется либо для объявления локальных переменных, когда требуется явно указать тип, либо для объявления переменных, которым значения будут присвоены позже.

💡":=" — это объявление, а "=" — присваивание. Попытка повторно использовать оператор ":=" для присваивания нового значения существующей переменной завершится ошибкой во время компиляции.

💡Если краткая форма объявления содержит слева смесь новых и существующих переменных, то она действует как форма присваивания новых значений существующим переменным.
👍5😁3