Библиотека Go для собеса | вопросы с собеседований
6.84K subscribers
221 photos
5 videos
1 file
402 links
Вопросы с собеседований по Go и ответы на них.

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

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

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

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
В чём суть и контекст применения Singleton в Go

Паттерн Singleton (Одиночка) — это порождающий паттерн проектирования, цель которого — гарантировать, что структура (или тип) имеет только один экземпляр и предоставляет глобальную точку доступа к нему.

➡️Когда использовать:
• Централизованное управление ресурсами: пул соединений, логгеры, кэш
• Избегание дублирования состояния в памяти
• Работа в многопоточной среде и потокобезопасный доступ к общей структуре


➡️ Когда не стоит использовать:
• Singleton нарушает SRP (единственная ответственность)
• Затрудняет тестирование (невозможно подменить зависимости)
• Провоцирует на глобальные сингл-точки сбоя (если объект упал — всё развалилось)
• Препятствует масштабируемости при переходе к микросервисной архитектуре


➡️ Важно: в Go нет классов и private конструкторов как в OOP-языках, поэтому реализация синглтона — технически иной подход, обычно через package-level переменные, sync и функции.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🌚2👏1
Какой контекст выбрать: context.Background() или context.TODO()

Когда вы работаете с Go-контекстами, легко запутаться: оба выглядят одинаково, оба ничего не делают — но используются по-разному.

context.Background() — это ваш "нулевой" контекст, от которого принято строить всё остальное. Он используется в main(), в инициализации, в серверах и корневых вызовах. Это базовая точка отсчёта.

context.TODO() — маркер, который говорит: "я ещё не знаю, какой контекст здесь будет, но он точно появится".
Такой контекст часто используется в заготовках, шаблонах, временном коде, пока архитектура не определена.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱32
Почему ленивую инициализацию важно делать безопасно в многопоточном окружении

Для ленивой инициализации (lazy init) в многопоточной среде можно использовать sync.Mutex. Это даёт максимальный контроль, но требует аккуратности.

Рабочий пример:
package main

import (
"fmt"
"sync"
)

type singleton struct {
data string
}

var (
instance *singleton
mu sync.Mutex
)

func GetInstance() *singleton {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &singleton{data: "initialized"}
}
}
return instance
}

func main() {
s := GetInstance()
fmt.Println("Singleton data:", s.data)
}


Двойная проверка (Double-Checked Locking):
1️⃣ if instance == nil не требует блокировки и отсекает большинство вызовов
2️⃣ внутри mu.Lock() — необходима, чтобы избежать гонки между горутинами, прошедшими первую проверку одновременно

➡️ Рекомендуется применять только когда нужно встроить логику, контроль или откладку внутрь блокировки. В остальных случаях предпочтительнее sync.Once

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
4👏1
Какие основные типы context в Go вы знаете

Go предоставляет функции, которые позволяют создавать производные контексты на основе базового
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()


WithCancel — вручную отменяет контекст
WithTimeout — отменяет через заданный интервал времени
WithDeadline — отменяет к заданному моменту времени
WithValue — добавляет пару ключ/значение в контекст (использовать осторожно)

Каждая из этих функций создаёт дочерний контекст, и если родитель будет отменён — отменятся и все дочерние.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🥱1
Зачем в реализации Singleton использовать sync.Once, если уже есть мьютексы

Если вы хотите простую, лаконичную и безопасную реализацию — используйте sync.Once
package main

import (
"fmt"
"sync"
)

type singleton struct {
config string
}

var (
once sync.Once
instance *singleton
)

func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{config: "default"}
})
return instance
}

func main() {
s := GetInstance()
fmt.Println("Singleton config:", s.config)
}

sync.Once гарантирует, что переданная функция будет выполнена строго один раз, даже если GetInstance вызывается из множества горутин.

Потокобезопасность на уровне стандартной библиотеки
Лаконичность кода
Не требует мьютексов или ручных проверок

Не подходит, если вам нужно сбрасывать или пересоздавать Singleton (например, в тестах)
Once работает только один раз за весь жизненный цикл приложения

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍2
Как правильно использовать ctx.Done() внутри горутин и что произойдёт, если этого не делать

Контекст используется как сигнальный канал. Правильно обрабатывать ctx.Done() нужно всегда, особенно в горутинах
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("goroutine stopped:", ctx.Err())
return
case <-time.After(1 * time.Second):
fmt.Println("working...")
}
}
}(ctx)


Если не слушать ctx.Done(), вы получите:
• Утечки горутин
• Зависание приложения
• Недетерминированное поведение при отмене


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍2
Как вам вопросы прошедшей недели

Оцените их по шкале 🔥,❤️,👍,😢, 🥱,
где 🔥 — это супер, а 🥱 — это скучно.

Также приветствуется фидбек в комментариях.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍24🔥137🥱1
Почему инициализация через init() не считается ленивой

Иногда экземпляр Singleton можно создавать заранее, при старте пакета. В Go для этого существует функция init().

Singleton с init() в действии
package main

import (
"fmt"
)

type singleton struct {
config string
}

var instance *singleton

func init() {
instance = &singleton{config: "preloaded"}
}

func GetInstance() *singleton {
return instance
}

func main() {
s := GetInstance()
fmt.Println("Singleton config:", s.config)
}


➡️ Особенности подхода:
• Инициализация происходит один раз, до выполнения main() — автоматически
• Синхронизация не требуется init() вызывается в однопоточном контексте
• Порядок инициализации между пакетами гарантирован Go-рантаймом

➡️ Подходит для простых случаев:
Объект всегда нужен в программе
Конфигурация не зависит от внешнего ввода
Важна простота, а не гибкость

➡️ Недостатки
Нарушает ленивую загрузку — объект создаётся даже если не используется
Затрудняет подмену или настройку из внешнего источника (например, через флаги, файлы, ENV)
Может ограничить тестируемость и повторную инициализацию

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
😁42👍1
Как правильно использовать context в юнит-тестах

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

Чтобы избежать зависаний и утечек в юнит-тестах, всегда создавайте context с таймаутом:
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()


Это гарантирует:
Тест не будет висеть бесконечно
Ресурсы будут высвобождены
Горутины получат сигнал на завершение


Если ваш тест зависает — это может говорить о том, что где-то в коде игнорируется ctx.Done()

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Какой способ инициализации Singleton в Go выбрать: sync.Mutex, sync.Once или init()

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍3
Зачем использовать select, если можно просто читать из канала

В Go оператор select — это конкурентный аналог switch, предназначенный исключительно для работы с каналами.

С его помощью можно:
• Ждать сразу несколько операций с каналами (чтение/запись)
• Управлять конкурентными потоками без блокировок
• Не блокироваться, если добавить
default ветку

select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case ch2 <- 42:
fmt.Println("Sent 42 to ch2")
default:
fmt.Println("Nothing ready")
}


Если ch1 или ch2 готовы — будет выполнен соответствующий case.
Если ни один канал не активен — выполняется default, и select не блокирует выполнение.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍52🤔1
Можно ли сделать Singleton тестируемым без нарушения его природы

Singleton может мешать тестированию, так как его состояние живёт весь runtime. Но есть обходные пути:

1️⃣ Вынос в интерфейс
type Storage interface {
Get(key string) string
}


2️⃣ Инъекция зависимости через параметр
func ProcessData(s Storage) { ... }


3️⃣ Сброс состояния Singleton (в тестах)
// В тестовых сборках
func resetSingleton() {
instance = nil
once = sync.Once{}
}


Warning: сброс Singleton — антипаттерн, но допустим в юнит-тестах

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🤔1
Как реализовать приоритет между каналами, если select выбирает случайно

Одно из важных свойств select в Go — рандомность выбора, если сразу несколько каналов готовы к операции.
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
}


Если и ch1, и ch2 доступны — Go случайным образом выберет один case.
Это исключает жёсткий приоритет каналов и распределяет нагрузку справедливо (не детерминированно).

Это нужно для предотвращения "голодания" менее приоритетных каналов, также это позволяет реализовать честные очереди и worker pool без ручного балансировщика

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
😁10👍3🌚2
Что такое отношение happens-before в модели памяти Go

Отношение happens-before гарантирует упорядоченность и видимость операций между разными горутинами. Если операция A happens-before операции B, то все записанные до A значения памяти гарантированно будут видны при выполнении B.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
😁3🎉1
Как вам вопросы прошедшей недели

Оцените их по шкале 🔥,❤️,👍,😢, 🥱,
где 🔥 — это супер, а 🥱 — это скучно.

Также приветствуется фидбек в комментариях.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25🔥11😢75👾1
Как Redis реализует сохранение данных на диск

Redis поддерживает два механизма сохранения данных: Redis Database Dump, который сохраняет данные в момент времени, и Append-Only File, который записывает каждую операцию записи.

Эти два метода могут использоваться вместе для достижения компромисса между производительностью и надежностью.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥3
Какие правила happens-before действуют для атомарных операций из пакета sync/atomic

Атомарная операция Store happens-before последующей атомарной операции Load той же переменной (при отсутствии иных атомарных модификаций между ними).

Инициализация всех глобальных переменных happens-before старту функции main(), что гарантирует корректные первоначальные значения при запуске программы.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
Как работает паттерн «Функциональные опции» в Go и как его можно реализовать

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

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

Пример без функциональных опций:
type Server struct {
host string
port int
protocol string
}

func NewServer(host string, port int) *Server {
return &Server{host: host, port: port, protocol: "http"}
}

Этот код создает сервер с параметрами host и port, но с изменениями в требованиях необходимо менять сигнатуру функции, что неудобно.

Реализация функциональных опций
1️⃣ Определим тип для опций:
type ServerOption func(*Server)


2️⃣ Функция для изменения порта:
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}


3️⃣ Модифицируем функцию NewServer:
func NewServer(host string, opts ...ServerOption) *Server {
server := &Server{host: host, port: 443, protocol: "https"}
for _, opt := range opts {
opt(server)
}
return server
}


Теперь можно гибко настроить параметры:
server1 := NewServer("localhost")               // с портом по умолчанию
server2 := NewServer("localhost", WithPort(8080)) // с портом 8080

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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🤔7👏1
Какова цель и принцип работы планировщика Go на базовом уровне

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

➡️ Модель M:N — в Go используется модель M:N, при которой M горутин могут быть назначены на N потоках ОС. Это позволяет создавать тысячи горутин с минимальными накладными расходами, связанными с управлением реальными потоками операционной системы.

➡️ G, M и P:
• G (goroutine) — горутина, выполняющаяся в рамках Go-программы
• M (machine thread) — поток ОС, который выполняет горутины
• P (processor) — контекст выполнения, который управляет очередью горутин и другими необходимыми данными. Обычно количество P соответствует числу ядер процессора.

➡️ Работа планировщика — во время выполнения горутины, она захватывает P и привязывается к M. Если горутина блокируется (например, из-за ожидания ввода/вывода), P освобождается и может быть назначен другой горутине, которая готова к выполнению.

➡️ Адаптация под нагрузку — планировщик динамически увеличивает или уменьшает количество потоков ОС (M) в зависимости от текущей нагрузки и блокировок.

➡️ Прерывание горутин — горутины могут быть прерваны планировщиком, чтобы освободить поток ОС для других горутин. Это гарантирует, что одна горутина не будет занимать процессорное время слишком долго, давая возможность другим горутинам быть выполненными.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9🤔2
Что представляют собой дженерики в языке Go и в чем их преимущества

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

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

➡️ Преимущества:

• Уменьшение дублирования кода с помощью дженериков можно создать одну функцию или структуру, которая будет работать с любыми типами данных.

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

• Увеличение производительности, избавление от преобразования типов позволяет улучшить скорость выполнения.

➡️ До дженериков

До введения дженериков, разработчики использовали интерфейсы и преобразование типов для обеспечения гибкости. Однако это часто приводило к потере производительности и снижению безопасности типов. Например, для создания функции поиска минимального значения в срезе, нужно было писать отдельные реализации для каждого типа:
func MinInts(arr []int) int { /*...*/ }
func MinFloats(arr []float64) float64 { /*...*/ }


➡️ С дженериками

Теперь с использованием дженериков можно создать одну универсальную функцию, которая работает с различными типами данных. Пример с функцией для нахождения минимального значения:
func Min[T comparable](arr []T) T { /*...*/ }

Здесь T — это параметр типа, а comparable указывает, что тип должен поддерживать операцию сравнения.

➡️ Пример с обобщенной структурой данных

Допустим, вам нужно создать структуру данных типа очередь (Queue).

Без дженериков:
type Queue struct {
data []interface{}
}

Использование interface{} требует преобразования типов, что снижает производительность.

С дженериками:
type Queue[T any] struct {
data []T
}

Здесь T может быть любым типом, и нет необходимости в преобразованиях.

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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17