Библиотека Go для собеса | вопросы с собеседований
7.42K subscribers
258 photos
10 videos
1 file
766 links
Вопросы с собеседований по Go и ответы на них.

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

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

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

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
Что такое паттерн Circuit Breaker и зачем он нужен

Circuit Breaker это защитный паттерн для распределённых систем. Он автоматически «отключает» вызовы к нестабильному сервису, чтобы предотвратить каскадные сбои и снизить нагрузку на и без того падающий узел.

Аналогия проста: как автоматический выключатель в электрощитке защищает проводку от перегрузки, так и Circuit Breaker защищает систему от цепной реакции ошибок.

Два состояния

Замкнуто — штатный режим. Все запросы проходят в обычном режиме. Счётчик ошибок растёт при каждом сбое.

Разомкнуто — защитный режим. После превышения порога ошибок паттерн перестаёт вызывать сервис и сразу возвращает ошибку. Сервис получает время на восстановление.

Реализация:

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

func Breaker(circuit Circuit, failureThreshold uint) Circuit {
var consecutiveFailures int = 0
var lastAttempt = time.Now()
var m sync.RWMutex

return func(ctx context.Context) (string, error) {
m.RLock()
d := consecutiveFailures - int(failureThreshold)

if d >= 0 {
// Экспоненциальная выдержка: 2, 4, 8... секунд
shouldRetryAt := lastAttempt.Add(time.Second * 2 << d)
if !time.Now().After(shouldRetryAt) {
m.RUnlock()
return "", errors.New("service unreachable")
}
}
m.RUnlock()

response, err := circuit(ctx)

m.Lock()
defer m.Unlock()

lastAttempt = time.Now()
if err != nil {
consecutiveFailures++
return response, err
}

consecutiveFailures = 0 // Успех — сбрасываем счётчик
return response, nil
}
}


Что важно в этой реализации

sync.RWMutex защищает общее состояние при конкурентных вызовах

• Экспоненциальная выдержка (2 << d) даёт сервису всё больше времени на восстановление с каждой неудачной попыткой

• После успешного вызова счётчик сбрасывается — цепь «замыкается» обратно автоматически

• Функция возвращает тот же тип Circuit, что позволяет прозрачно встраивать Breaker без изменения кода клиента

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😁1
Что такое package aliasing

Package aliasing это возможность присвоить псевдоним импортируемому пакету прямо в блоке import. Полезен в конкретных ситуациях, но злоупотреблять им не стоит: лишние алиасы усложняют чтение кода.

Синтаксис:
import fm "fmt"

fm.Println("hello") // вместо fmt.Println


Алиас указывается перед путём к пакету и полностью заменяет его имя в текущем файле.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Когда использовать package aliasing

📌 Кейсы использования

1. Конфликт имён

Два пакета с одинаковым именем. Без алиасов код не скомпилируется.
import (
sqlDB "project/sql/db"
nosqlDB "project/nosql/db"
)


2. Длинный путь импорта

Сокращает шум при частом обращении к пакету с громоздким путём:
import (
mh "myproject/subproject/module/helpers"
)


3. Неудобное или неочевидное имя пакета

Актуально для сгенерированного кода, когда имя пакета не совпадает с тем, что ожидает читатель:
import (
validator "github.com/myorg/gen/v2/validate_pb"
)


4. Пустой импорт _

Специальный алиас для импорта ради побочных эффектов: регистрация драйвера, init()-функция; без использования пакета в коде:
import _ "github.com/lib/pq" // регистрирует PostgreSQL-драйвер


5. Импорт в текущее пространство имён .

Позволяет обращаться к экспортируемым именам пакета без префикса:
import . "math"

r := Sqrt(16) // вместо math.Sqrt(16)


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Что такое указатель на указатель

Указатель хранит адрес переменной. Указатель на указатель хранит адрес другого указателя. Каждый уровень добавляет одну «звёздочку» к типу и одно разыменование для доступа к значению.

Пример:
a := 100
var b *int = &a // b хранит адрес a
var c **int = &b // c хранит адрес b


Цепочка в памяти выглядит так:
c → b → a → 100


Разыменование:
fmt.Println(a)   // 100          — исходное значение
fmt.Println(b) // 0xc000014090 — адрес a
fmt.Println(*b) // 100 — значение a через b
fmt.Println(c) // 0xc00000e028 — адрес b
fmt.Println(*c) // 0xc000014090 — адрес a через c
fmt.Println(**c) // 100 — значение a через c


Каждая * это один шаг по цепочке адресов.

**c позволяет не только читать значение a, но и менять сам указатель b, то есть переключать его на другую переменную. Именно это делает двойной указатель полезным, а не просто экзотикой.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5🌚5😁32
Указатель на указатель: когда это реально нужно

Двойной указатель решает конкретную задачу: изменить указатель внутри функции так, чтобы изменение было видно снаружи.

Проблема без **:
func resetPointer(p *int) {
newVal := 0
p = &newVal // меняем локальную копию — снаружи ничего не изменится
}


p — это копия адреса. Переназначение p внутри функции не затрагивает оригинал.

Решение через **int:
func resetPointer(p **int) {
newVal := 0
*p = &newVal // меняем сам указатель — изменение видно снаружи
}

func main() {
a := 42
ptr := &a
resetPointer(&ptr)
fmt.Println(*ptr) // 0
}


Теперь функция получает адрес самого указателя и может подменить его цель.

Другие кейсы

Динамические структуры данных — при построении связных списков или деревьев, когда нужно менять корневой указатель:
func insertHead(head **Node, val int) {
newNode := &Node{val: val, next: *head}
*head = newNode
}


Инициализация через функцию — паттерн, когда объект создаётся внутри функции и возвращается через параметр, а не через return:
func initConfig(cfg **Config) {
*cfg = &Config{Timeout: 30}
}


Когда не нужен

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

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔5👾32🌚2👍1
Как сделать кастомного обработчика логов с log/slog

Нужно реализовать интерфейс slog.Handler: четыре метода, которые дают контроль над форматом, фильтрацией и транспортом логов:
type Handler interface {
Enabled(context.Context, Level) bool
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}


Enabled вызывается до того, как рантайм начнёт вычислять аргументы лога. Если возвращает false, то Handle вообще не вызывается. Это точка оптимизации: дорогие вычисления в аргументах не будут выполнены для отфильтрованных уровней.

Handle — основной метод. Получает Record с временем, уровнем, сообщением и атрибутами. Атрибуты итерируются через коллбэк r.Attrs(fn), а не через срез и это намеренная оптимизация аллокаций, первые несколько атрибутов хранятся инлайн. Метод должен быть goroutine-safe, поэтому нужен мьютекс на запись в io.Writer.

WithAttrs вызывается при logger.With("key", "val"). Должен вернуть новый handler с сохранёнными полями — мутировать текущий нельзя, логгер может использоваться из нескольких горутин.

WithGroup вызывается при logger.WithGroup("request"). Все последующие атрибуты должны быть вложены под этим ключом: request.method, request.ip.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
Как ведут себя срезы в Go на граничных значениях

Срез это не массив, а заголовок из трёх полей: указатель на данные, длина и ёмкость. Это важно понимать, прежде чем говорить о границах.

Правило границ. При нарезке a[low:high] должно выполняться 0 <= low <= high <= cap(a). Примечательно, что high ограничен именно cap, а не len — это позволяет «заглянуть» вперёд за текущую длину, если базовый массив это допускает.

Нарушение границ — паника в рантайме. Компилятор не проверяет корректность индексов — это делает рантайм. a[0:len(a)+1] скомпилируется, но упадёт с slice bounds out of range при выполнении.

Пустой срез — не nil. a[2:2] — валидный срез с длиной 0. Он инициализирован и указывает на память. var s []int — другое: nil-срез, у которого указатель равен nil. len и cap у обоих равны нулю, но s == nil вернёт true только для первого.

Разделяемая память. Срезы, нарезанные от одного массива, указывают на те же данные. Запись через один срез изменит то, что видит другой — до тех пор, пока не произошёл рост.

Трёхиндексная нарезка. a[low:high:max] задаёт ёмкость результата явно: cap = max - low. Используется, чтобы append не «прорвался» за нужный регион и не затронул соседние данные в исходном массиве.

Рост при append. Когда len == cap, Go создаёт новый массив, копирует данные и возвращает срез с новым указателем. С этого момента два среза больше не делят память — и это один из самых частых источников неожиданного поведения на практике.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Чем отличаются Lock-Free и Wait-Free алгоритмы

Lock-Free алгоритмы это безмьютексные структуры данных, где хотя бы один поток всегда завершает операцию за конечное число шагов:

• Гарантия: система в целом прогрессирует (нет глобальной блокировки).

• Минус: некоторые потоки могут «застревать» в бесконечных retry-циклах (livelock).

• Плюсы: высокая производительность, проще в реализации.

Wait-Free алгоритмы это строгий подкласс lock-free, где каждый поток завершает операцию за конечное число шагов независимо от других:

• Гарантия: индивидуальный прогресс для всех (полная справедливость, нет голодания ресурсов).

• Минус: сложнее реализовать, ниже производительность из-за оверхеда на координацию.

• Когда использовать: в реал-тайм системах, например, ABA-free структуры.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🥱1
Как передавать сигналы в каналах

Первый и самый простой это пустая структура. Она не занимает памяти и означает только одно: событие произошло:
done := make(chan struct{})

go func() {
// работа выполнена
close(done)
}()

<-done // ждём сигнала


close — лучший способ сигналить завершение, потому что все читатели получат сигнал одновременно.

Второй способ это передать булево значение или ошибку, когда важно передать не только факт события, но и его результат:
result := make(chan error, 1)

go func() {
err := doSomething()
result <- err
}()

if err := <-result; err != nil {
log.Fatal(err)
}


Третий способ — context.Context. Это стандарт в продакшн-коде. Контекст несёт в себе сигнал отмены, дедлайн и значения.

Внутри он тоже использует канал:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

select {
case result := <-work(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
//go:build vs // +build — в чём разница

// +build — старая форма, использовалась до Go 1.17. Требует пустой строки после блока директив, а логика объединяется через пробелы (AND) и запятые (OR), что легко перепутать:
// +build linux darwin
// +build amd64

package main


//go:build это новая форма, появилась в Go 1.17. Синтаксис стал читаемым: используются обычные логические операторы &&, ||, !:
//go:build (linux || darwin) && amd64

package main


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍71
Почему в go switch отличается от других языков

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

В Go каждый case автоматически завершается — никакого проваливания:
switch status {
case 1:
fmt.Println("one") // выполнится только это
case 2:
fmt.Println("two") // сюда не попадём
}


В C/Java без break выполнились бы оба блока.

Есть fallthrough, C-стайл поведение можно воспроизвести явно:
switch status {
case 1:
fmt.Println("one")
fallthrough // явно говорим "провалиться" дальше
case 2:
fmt.Println("two") // выполнится тоже
}


Несколько значений в одном case:
switch day {
case "Saturday", "Sunday":
fmt.Println("выходной")
case "Monday", "Friday":
fmt.Println("почти выходной")
}


🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5
🏃‍♀️ Мы собрали бесплатный мега-гайд по ии-агентам 👇

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

Часть 1. Введение, юзкейсы и реальность
Разбираемся с терминами, снимаем розовые очки и смотрим, где ИИ реально приносит бабки, а где только жжет нервы:

1. «Так что вообще считается AI-агентом?»
2. «Где тут бот, а где уже AI-агент?»
3. «Не надо пихать AI-агента в каждую задачу»
4. «Что уже можно спокойно делать через AI-агентов?»
5. «А что через AI-агентов пока лучше не трогать?»

Часть 2. Изнанка, ошибки и архитектура
Как всё это устроено под капотом, чтобы не слить бюджет и не наломать дров на старте:

6. «Можно ли просто сесть вечером и собрать себе AI-агента?»
7. «С чего вообще начать, если хочется попробовать AI-агентов»
8. «Почему AI-агент может внезапно начать творить дичь»
9. «Где AI-агенты реально экономят время, а где только добавляют возни»
10. «Почему они жрут столько денег?»

Часть 3. Хардкорная практика (Что делать руками)
Хватит теории. Открываем ноут, запускаем Cursor и делаем нормальные, отказоустойчивые системы:

11. «Почему одного промпта мало?»
12. «Почему AI-агенту мало просто “дать доступ к данным”»
13. «Если не следить за AI-агентом, он быстро начинает жить своей жизнью»
14. «Собрать демку легко. Но как же сделать нормально»
15. «Как сделать, чтобы это не развалилось через неделю?»

👍 Сохраняйте пост в избранное, чтобы не потерять.

🤫 А завтра стартует наш курс по ии-агентам
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Что происходит со стеком горутины при росте

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

Для производительности важны два момента.

1. Копирование не бесплатно. Если горутина постоянно растёт и сжимается на границе текущего размера, рантайм будет копировать снова и снова. Это stack thrashing и в горячем пути это даёт ощутимый overhead.

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

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

Два указателя, движущихся навстречу друг другу:
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}


Начиная с Go 1.21 в стандартной библиотеке есть готовая функция slices.Reverse(), которая делает то же самое под капотом:
import "slices"

s := []int{1, 2, 3, 4, 5}
slices.Reverse(s) // [5, 4, 3, 2, 1]


Оба варианта работают in-place, без выделения дополнительной памяти.

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

Префиксная сумма это массив, где каждый элемент prefix[i] хранит сумму всех элементов исходного массива от 0 до i включительно.

Построение:
func buildPrefix(arr []int) []int {
prefix := make([]int, len(arr)+1)
for i, v := range arr {
prefix[i+1] = prefix[i] + v
}
return prefix
}

func rangeSum(prefix []int, l, r int) int {
return prefix[r+1] - prefix[l]
}


Задача: количество подмассивов с суммой K

func subarraySum(nums []int, k int) int {
count := 0
prefix := 0
seen := map[int]int{0: 1}

for _, v := range nums {
prefix += v
count += seen[prefix-k] // если ключа нет — вернёт 0, это фича Go
seen[prefix]++
}

return count
}


Где использовать:

• Запросы суммы на отрезке — самый очевидный случай. Если массив не меняется, а запросов много, строишь префикс один раз и отвечаешь за O(1).

• Поиск подмассива с заданной суммой — сводишь к задаче "найти два индекса префикса с разностью K", решается через хэшмап за O(n).

• Задачи на чётность/нечётность суммы — считаешь префикс по модулю 2, ищешь совпадения.

• Задачи на матрицах — 2D префикс даёт сумму любого прямоугольника за O(1), используется в задачах с изображениями, тепловыми картами, grid-задачах.

• Sliding window с условием на сумму — иногда проще через префикс, чем двумя указателями, особенно если окно не фиксированное.

🐸 Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Какие преимущества даёт синглтон в Go

+ Единая точка доступа к ресурсу

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

+ Ленивая инициализация через sync.Once

В Go синглтон удобно реализуется через sync.Once: объект создаётся только при первом обращении. Это экономит память и время запуска, если ресурс вообще не понадобится в ходе работы программы.

+ Глобальное состояние без глобальных переменных

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

+ Потокобезопасная инициализация из коробки

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

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

- Проблемы с потокобезопасностью

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

- Проблемы с масштабируемостью

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

- Проблемы с производительностью

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

- Проблемы с инициализацией синглтона

Если синглтон требует сложной инициализации, например, создание нескольких объектов, настройка зависимостей, это может замедлить работу приложения, особенно если инициализация не оптимизирована.

🐸Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🥱2
Чем OnceValue лучше глобальной переменной с init()

init() запускается при старте программы, даже если значение никогда не понадобится.

OnceValue — ленивая инициализация: вычисление происходит только при первом вызове. Это полезно для тяжёлых операций (соединения с БД, чтение файлов) и упрощает тестирование.

🐸Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Где и когда уместно применять синглтон в Go

Пул соединений с базой данных

Создание нового соединения это дорогостоящая операция. Синглтон гарантирует, что во всём приложении используется один общий пул соединений (*sql.DB), что снижает накладные расходы и предотвращает исчерпание лимитов на стороне базы данных.

Менеджер конфигурации

Параметры приложения: файл конфигурации, переменные окружения. Они читаются один раз при старте. Синглтон даёт удобный единый доступ к этим данным из любой части программы без повторного чтения и парсинга.

Логгер

Глобальный логгер: он должен быть один, настроен единожды (уровень логирования, вывод, формат) и доступен из любого пакета. В Go это часто реализуется через log/slog или сторонние библиотеки вроде zap.

Кэш в памяти

Если приложению нужен разделяемый кэш (например, результаты тяжёлых вычислений или ответы внешних API), синглтон обеспечивает единое хранилище для всех горутин. Важно сочетать его с sync.RWMutex или использовать sync.Map для безопасного доступа.

🐸Библиотека Go для собеса
Please open Telegram to view this post
VIEW IN TELEGRAM
1
🏃‍♀️ Как провести вечер вторника с пользой для карьеры?

Включайте кружок там личное приглашение от спикера. 👆

Уже завтра в прямом эфире, разбираем архитектуру контекста в мультиагентных системах.

🤫 Секретный лут:
промик на 5.000₽. Он достанется только тем, кто придет на прямой эфир.

👉 Регистрируйтесь на трансляцию
Please open Telegram to view this post
VIEW IN TELEGRAM