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

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

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

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

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
Почему инициализация через 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🔥12😢75👾1
Как Redis реализует сохранение данных на диск

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

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

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

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

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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Как работает паттерн «Функциональные опции» в 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
В чем заключается принцип работы Escape analysis в Go

Escape analysis — это техника компилятора Go, предназначенная для определения оптимального местоположения переменных: в стеке или в куче. Этот процесс критичен для повышения производительности программы и эффективного использования памяти.

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

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

Если же переменная «выходит» — например, возвращается из функции или сохраняется в глобальной переменной — она будет размещена в куче, и за её освобождение будет отвечать сборщик мусора.

➡️ Переменная остается в рамках функции (стек):
func sum(a, b int) int {
result := a + b
return result
}

В данном случае переменная result остается в области видимости функции, поэтому она будет размещена на стеке.

➡️ Переменная выходит за пределы функции (куча):
func newInt() *int {
result := 42
return &result
}

Здесь переменная result выходит за пределы функции, потому что возвращается указатель на неё. Поэтому она будет размещена в куче.

➡️ Переменная сохраняется в глобальной переменной (куча):
var globalVar *int

func setGlobalVar() {
x := 100
globalVar = &x
}

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

Escape analysis помогает управлять памятью, решая, где лучше хранить переменные — в стеке или в куче.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9👏2🤔1
🔥 Вы ещё можете застать старый добрый Proglib — с вечным доступом к курсам.

С 1 августа всё меняется: навсегда — останутся только те, кто успел купить сейчас.

-40% на все курсы. Включая обновлённый Python (кроме курса по AI-агентам)

Это не просто распродажа. Это — последняя точка входа в Proglib Academy по старым правилам.

📚 Выбрать и забрать свой курс навсегда → https://clc.to/TBtqYA
Как вам вопросы прошедшей недели

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

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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👍21
Каким образом в Go реализуется паттерн «Прототип»

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

В Go этот паттерн может быть реализован через интерфейс, который описывает метод клонирования. Объект-прототип реализует этот метод, создавая новый экземпляр с теми же данными, что и оригинал.

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

Чтобы реализовать этот паттерн, мы будем использовать интерфейс Element, который будет представлять как файлы, так и директории. Каждая структура должна реализовывать два метода:
1. display — для отображения информации о текущем элементе.
2. clone — для создания копии элемента.


➡️ Пример 1: Файл (Document)
Файл в нашей файловой системе может быть представлен типом Document. Он имеет имя (например, Doc1), и когда мы хотим его клонировать, мы добавляем к имени суффикс "_copy" (например, Doc1_copy).

➡️ Пример 2: Директория (Directory)
Директория представляет собой коллекцию элементов, таких как файлы или другие директории. Она может содержать несколько вложенных объектов и поддерживает тот же интерфейс Element. Когда мы клонируем директорию, мы создаем копию самой директории, а также клонируем все ее элементы.

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

Рекурсивное клонирование — объект является директорией, клонирует не только саму директорию, но и все ее элементы, повторяя этот процесс для каждой вложенной директории и ее элементов.

Суффикс "_copy" клон получает суффикс "_copy" в своем имени, что позволяет легко отличить оригинальный объект от его копии. (Doc1 → Doc1_copy)


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍73🤩2
Что подразумевается под inlining в Go и какова его цель

Inlining — это оптимизация, при которой компилятор заменяет вызовы функции её телом, чтобы уменьшить накладные расходы, связанные с вызовом. Это помогает ускорить выполнение программы.

Пример:
func add(a, b int) int {
return a + b
}

func main() {
result := add(3, 4)
fmt.Println(result)
}


После inlining код может выглядеть так:
func main() {
result := 3 + 4
fmt.Println(result)
}


Основные моменты:
1️⃣Автоматическое решение компилятора, Go сам решает, следует ли выполнить inlining для функции, основываясь на её размере и сложности.
2️⃣Основное преимущество — это ускорение работы программы за счет снижения накладных расходов при многократных вызовах функции.
3️⃣Ограничения: не все функции можно встроить. Большие функции или те, что вызывают другие функции, скорее всего, не будут встроены. Чрезмерное использование inlining может увеличить размер исполняемого файла.
4️⃣Как увидеть решения компилятора — с помощью флага -gcflags="-m" можно увидеть, какие оптимизации и встраивания принял компилятор.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
8
Как в Go передаются параметры в функцию

➡️В Go параметры всегда передаются по значению. Это означает, что при передаче аргумента в функцию, функция получает копию исходного значения.

Чтобы работать с оригинальной переменной, а не с её копией, нужно передавать адрес этой переменной. Однако сам указатель будет передан по значению.

Передача базовых типов данных (int, string, bool и т.д.)
func modifyValue(x int) {
x = x * 2
}

func main() {
num := 10
modifyValue(num)
fmt.Println(num) // 10
}

В примере значение переменной num не изменяется, потому что в функцию передается копия этого значения.

Передача срезов и мап

Срезы и мапы передаются по ссылке, то есть функция получает ссылку на оригинальный объект. Изменения внутри функции отразятся на оригинале.
func modifySlice(s []int) {
s[0] = 100
}

func main() {
numbers := []int{1, 2, 3}
modifySlice(numbers)
fmt.Println(numbers) // [100 2 3]
}


Передача указателей

Указатели позволяют передавать ссылку на память, где хранится значение. Изменения через указатель будут видны за пределами функции.
func modifyWithPointer(x *int) {
*x = *x * 2
}

func main() {
num := 10
modifyWithPointer(&num)
fmt.Println(num) // 20
}


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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍61
Что представляет собой оператор select в Go и как он взаимодействует с каналами

Оператор select в Go напоминает конструкцию switch, но предназначен исключительно для работы с каналами. Он позволяет управлять несколькими каналами, одновременно ожидая данных или отправляя их.

➡️ Ключевые особенности select

• Ожидание нескольких каналов — select позволяет ожидать данные сразу из нескольких каналов или проверять возможность отправки данных в канал.

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

• Рандомный выбор, если несколько каналов готовы к операции одновременно, select выберет один из них случайным образом.

Пример select
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
case ch3 <- 3:
fmt.Println("Sent 3 to ch3")
default:
fmt.Println("No communication")
}

В этом примере select будет ждать, пока не получит сообщение из одного из каналов ch1 или ch2, или не сможет отправить значение в канал ch3. Если ни одно из этих условий не выполнится, будет выполнен блок default, который сообщает о том, что нет доступной коммуникации.

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