PrepBro
Профессии
PrepBro
Профессия:

Подготовка

  • Вопросы2056
  • Задачи64

Аналитика

  • hh статистика
  • Анализ резюме

Практика

  • Тестовое собеседование
  • Mock-собеседование
  • Менторы

Поддержка / отзывы

Telegram админа
Профессия:

Подготовка

  • Вопросы2056
  • Задачи64

Аналитика

  • hh статистика
  • Анализ резюме

Практика

  • Тестовое собеседование
  • Mock-собеседование
  • Менторы

Поддержка / отзывы

Telegram админа
Все 24 профессии
Android DeveloperData AnalystSystem Analyst1С DeveloperiOS DeveloperBusiness AnalystJava DeveloperData ScientistQA EngineerQA AutomationPHP BackendC/C++ BackendDevOps EngineerIT Project ManagerFrontend DeveloperNode.js BackendUnity DeveloperC# BackendProduct AnalystFlutter DeveloperPython DeveloperIT Product ManagerGo DeveloperData Engineer

© 2026 PrepBro. Все права защищены.

Telegram-бот

Задачи по Go Developer

Задачи с собеседований на Go-разработчика: горутины и каналы, worker pool, rate limiter, graceful shutdown HTTP-сервера, предсказание вывода кода на slice, defer и nil-каналах, поиск багов вроде goroutine leak и data race в map. Каждая задача идёт с разбором и рабочим кодом.

Graceful Shutdown HTTP сервера
2.0 Middle🔥 30💬 1

Решение

Анализ задачи

Требования:

  • Запустить HTTP сервер
  • Перехватывать SIGINT и SIGTERM
  • Остановить приём новых соединений
  • Дождаться завершения активных запросов
  • Использовать context и timeout для корректного завершения

Стратегия:

  • Запустить сервер в горутине
  • Слушать сигналы в другой горутине
  • При получении сигнала вызвать Shutdown() с таймаутом

Решение

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)
Читать полностью ->
Mutex и WaitGroup на каналах
2.0 Middle🔥 29💬 1

Mutex и WaitGroup на каналах - полное решение

Описание задачи

В Go каналы являются примитивом синхронизации между горутинами. Да, можно реализовать и Mutex, и WaitGroup только на каналах, используя их свойства:

  • Отправка в канал блокирует отправителя, пока кто-то не прочитает
  • Чтение из канала блокирует читателя, пока кто-то не отправит
  • Закрытие канала разбудит всех ожидающих читателей

Часть 1: Mutex на каналах

package main

import (
    "fmt"
    "sync"
)

// ChannelMutex реализует взаимное исключение на каналах
type ChannelMutex struct {
    ch chan struct{}
}

func NewChannelMutex() *ChannelMutex {
    m := &ChannelMutex{
        ch: make(chan struct{}, 1), // буферизированный канал емкостью 1
    }
    m.ch <- struct{}{} // инициализируем токеном
    return m
}

// Lock получает взаимное исключение (блокирует, если занято)
func (m *ChannelMutex) Lock() {
    <-m.ch // читаем из канала, блокируемся если пусто
}
Читать полностью ->
Что выведет код? nil канал
2.0 Middle🔥 28💬 1

nil канал в select - полное решение

Ответы на вопросы

1. Что выведет программа?

default

Объяснение:

  • var ch chan int - объявляем переменную типа канал, но не инициализируем её
  • Неинициализированный канал имеет значение nil
  • При select из nil канала - операция не блокируется, вместо этого пропускается
  • Так как нет других успешных операций в select, выполняется default ветка

2. Что произойдет, если убрать default case?

var ch chan int

select {
case val := <-ch:
    fmt.Println(val)
}

Результат: Программа зависнет (deadlock).

Почему?

  • select из nil канала никогда не будет готов к выполнению
  • Это не паника, а просто вечная блокировка
  • Go runtime обнаружит deadlock и выведет ошибку:
    fatal error: all goroutines are asleep - deadlock!
    

Подробный анализ поведения nil каналов

Операции с nil каналом

var ch chan int  // ch == nil
Читать полностью ->
Что выведет код? Буферизованный канал
1.6 Junior🔥 28💬 1

Решение

Эта задача про буферизованные каналы и их поведение при отправке и получении данных. Ответ демонстрирует важное понимание concurrency в Go.

Ответ на вопросы

1. Что выведет программа?

1
2
3
4
5

Программа выведет все 5 чисел без ошибок и deadlock-а.

2. Будет ли deadlock? Почему?

НЕТ, deadlock-а НЕ будет, хотя могло бы быть в других сценариях.

Анализ работы буферизованного канала

Ключевое различие — буферизованный канал (make(chan int, 4)):

Безбуферный канал:  make(chan int)    — buffer size = 0
Буферизованный:     make(chan int, 4) — buffer size = 4

Как это работает:

ch := make(chan int, 4)  // буфер на 4 элемента
Читать полностью ->
Что выведет код? Pointer vs Value receiver
1.6 Junior🔥 28💬 1

Решение

Это задача про pointer vs value receivers и interface в Go. Классический подвох, который показывает важность понимания, как методы привязываются к типам.

Ответ на вопросы

1. Скомпилируется ли код?

НЕТ, код НЕ скомпилируется.

2. Какая будет ошибка?

compilation error:
Foo does not implement Bar (Get method has pointer receiver)

Ошибка компилятора говорит, что Foo не реализует интерфейс Bar, потому что метод Get() определён с pointer receiver *Foo, а не value receiver Foo.

3. Как исправить код?

Есть три варианта, каждый с разными implications:

Вариант 1: Передать указатель (рекомендуется)

func main() {
    foo := Foo{"hello"}
    print(&foo)  // передаём указатель вместо значения
}

Плюсы:

  • Не меняем определение метода
  • Явно показываем, что функция работает с указателями

Минусы:

  • Нужно помнить о передаче указателя

Вариант 2: Изменить receiver на value receiver

Читать полностью ->
Объединение каналов (Fan-In)
1.7 Middle🔥 26💬 1

Решение

Это классический паттерн Fan-In в Go — объединение нескольких каналов в один. Задача требует корректного управления жизненным циклом каналов и синхронизации горутин.

Подход

  1. Создаём выходной канал
  2. Для каждого входного канала запускаем горутину, которая читает из него
  3. Используем sync.WaitGroup для отслеживания завершения всех горутин
  4. Закрываем выходной канал, когда все входные каналы исчерпаны

Реализация

package main

import "sync"
Читать полностью ->
Thread-safe Map
2.2 Middle🔥 25💬 1

Решение

Thread-safe Map — это обёртка над стандартной map с синхронизацией доступа через RWMutex. RWMutex позволяет нескольким читателям работать одновременно, но исключает писателей.

Подход

  • Используем sync.RWMutex для синхронизации
  • Set/Delete: Lock() → эксклюзивный доступ
  • Get: RLock() → разделяемый доступ для читателей
  • Len: RLock() → читаем только размер

Реализация

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu    sync.RWMutex
    items map[string]interface{}
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        items: make(map[string]interface{}),
    }
}

func (m *SafeMap) Set(key string, value interface{}) {
    m.mu.Lock()  // эксклюзивная блокировка для записи
    defer m.mu.Unlock()
    m.items[key] = value
}

func (m *SafeMap) Get(key string) (interface{}, bool) {
    m.mu.RLock()  // разделяемая блокировка для чтения
    defer m.mu.RUnlock()
    val, ok := m.items[key]
    return val, ok
}
Читать полностью ->
Rate Limiter (Token Bucket)
2.0 Middle🔥 25💬 1

Решение

Token Bucket Rate Limiter — это алгоритм для ограничения частоты запросов. Используется в системах ограничения API, защиты от DDoS, и контроля нагрузки. Ключевая идея: токены пополняются с фиксированной скоростью, каждый запрос расходует токены.

Принцип работы

  1. Инициализация: в корзине максимум capacity токенов
  2. Пополнение: каждую секунду добавляется rate токенов (но не более capacity)
  3. Проверка доступа: если токенов достаточно, вычитаем их и разрешаем запрос
  4. Отказ: если токенов недостаточно, отказываем в запросе

Реализация

import (
    "sync"
    "time"
)

type RateLimiter struct {
    mu       sync.Mutex
    rate     float64
    capacity float64
    tokens   float64
    lastTime time.Time
}

func NewRateLimiter(rate float64, capacity int) *RateLimiter {
    return &RateLimiter{
        rate:     rate,
        capacity: float64(capacity),
        tokens:   float64(capacity),
        lastTime: time.Now(),
    }
}
Читать полностью ->
Синхронизация кэша
2.3 Middle🔥 24💬 1

Синхронизация кэша с TTL - полное решение

Описание задачи

Нужно реализовать потокобезопасный кэш, который:

  • Хранит значения с ограничением по времени (TTL)
  • Автоматически удаляет устаревшие записи
  • Поддерживает concurrent access через RWMutex
  • Корректно завершает фоновую горутину очистки

Архитектурное решение

Компоненты:

  1. RWMutex - для потокобезопасности (читать часто, писать редко)
  2. Структура Entry - хранит значение и время жизни
  3. *map[string]Entry - основное хранилище
  4. Фоновая горутина - периодически удаляет устаревшие записи
  5. Канал для shutdown - graceful завершение

Реализация

package main

import (
    "fmt"
    "sync"
    "time"
)

// Entry хранит значение и время его истечения
type Entry struct {
    Value     interface{}
    ExpiresAt time.Time
}
Читать полностью ->
Сумма квадратов с горутинами
2.0 Middle🔥 24💬 1

Решение

Анализ задачи

Требования:

  • Функция должна вычислить сумму квадратов чисел от 1 до c
  • Необходимо использовать горутины для параллельного выполнения
  • Нужны каналы для передачи результатов между горутинами
  • Требуется select для получения результатов

Математика: 1² + 2² + 3² + 4² + 5² = 1 + 4 + 9 + 16 + 25 = 55

Решение

func SumOfSquares(c int) int {
    // Канал для получения результатов
    resultChan := make(chan int)
    
    // Запускаем горутину для вычисления каждого квадрата
    for i := 1; i <= c; i++ {
        go func(num int) {
            square := num * num
            resultChan <- square
        }(i)
    }
    
    // Собираем результаты
    sum := 0
    for j := 0; j < c; j++ {
        // Используем select для получения результата из канала
        select {
        case result := <-resultChan:
            sum += result
        }
    }
    
    return sum
}

Пояснения реализации

Читать полностью ->
Что выведет код? Append и базовый массив
1.2 Junior🔥 24💬 1

Решение

Это задача на глубокое понимание того, как слайсы работают в Go, особенно взаимодействие append и shared backing array.

Ответ на вопросы

1. Что выведет программа?

[10 2 3] [1 10 3]

Отправка:

  • b = [10 2 3]
  • a = [1 10 3] (значение изменилось!)

2. Почему значение в a изменилось?

Потому что a[:1] и a делят один и тот же backing array (буферный массив).

Анализ кода

a := []int{1, 2, 3}

Внутреннее представление a:

slice a:
┌──────┬─────┬─────┐
│ ptr  │ 3   │ 3   │  ptr → [1, 2, 3] (буферный массив)
│      │ len │ cap │  len = 3, cap = 3
└──────┴─────┴─────┘
b := append(a[:1], 10)

Шаг 1: a[:1] создаёт новый слайс

slice a[:1]:
┌──────┬─────┬─────┐
│ ptr  │ 1   │ 3   │  ptr → [1, 2, 3] (ТОТЖЕ буферный массив!)
│      │ len │ cap │  len = 1, cap = 3
└──────┴─────┴─────┘
Читать полностью ->
Найти баг: Goroutine Leak
2.0 Middle🔥 24💬 1

Решение

Это классическая задача на goroutine leak — утечку горутин. Горутина остаётся запущенной даже после того, как её результаты больше не нужны.

1. Какая проблема?

Проблема: Goroutine Leak (утечка горутин)

Горотина запущенная в process() никогда не завершается:

for i := 0; ; i++ {  // ← бесконечный цикл
    ch <- i
    time.Sleep(100 * time.Millisecond)
}

В main читаются только 5 значений, затем программа выводит "done" и завершается, но горутина остаётся в памяти до выхода программы.

Последствия:

  • Утечка памяти (горутина занимает память)
  • Утечка системных ресурсов
  • В больших системах может привести к exhaust всех горутин

2. Как исправить?

Вариант 1: Закрыть канал

Читать полностью ->
Что выведет код? Slice и append
1.0 Junior🔥 24💬 1

Решение

Это классическая задача, которая показывает критическое различие между слайсами в Go. Ответ требует глубокого понимания того, как слайсы передаются в функции и как работает append.

Ответ на вопросы

1. Что выведет программа?

[]

Отправка: пустой слайс.

2. Почему?

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

  • Слайсы передаются по значению, но это не означает копирование содержимого
  • Каждый слайс — это структура с тремя полями: ptr (указатель на данные), len (длина), cap (capacity)
  • Когда вы передаёте слайс в функцию, копируется сама структура слайса, не данные

Что происходит в коде:

1. main: s = make([]int, 0, 2)
   → создаётся слайс {ptr→[_,_], len=0, cap=2}

2. main: doSomething(s)
   → копируется структура слайса в параметр a
   → a = {ptr→[_,_], len=0, cap=2} (копия, но указатель на те же данные)
Читать полностью ->
Что выведет код? defer и panic
1.0 Junior🔥 23💬 1

Решение: Что выведет код с defer и panic

Ответ на вопрос 1

Программа выведет:

C
B
A

Почему F не выводится?

Panic прерывает выполнение кода в текущей функции. Строка fmt.Println("F") и defer fmt.Println("E") никогда не выполнятся, потому что они находятся после вызова анонимной функции, которая паникует.

Пошаговое выполнение

Шаг 1: Регистрация defer в main

defer fmt.Println("A")  // defer stack: [A]
defer fmt.Println("B")  // defer stack: [B, A]

Deferы добавляются в стек LIFO (Last In, First Out).

Шаг 2: Вызов анонимной функции

func() {
    defer fmt.Println("C")  // defer stack (локальный): [C]
    panic("panic!")         // ПАНИКА!
    defer fmt.Println("D")  // Эта строка НЕ выполнится
}()

Шаг 3: Обработка паники

Паника вызывает раскрутку стека (unwinding):

  1. Выполняются все defer текущей функции в обратном порядке
  2. Паника распространяется вверх в вызывающую функцию
  3. Процесс повторяется
Читать полностью ->
Найти баг: WaitGroup и горутины
1.7 Middle🔥 22💬 1

Решение

Это задача с множественными проблемами: race condition, closure-захват переменной и race condition при доступе к переменной. Демонстрирует типичные ошибки с concurrency в Go.

1. Какие проблемы в коде?

Проблема 1: Closure захватывает переменную цикла

for i := 1000; i > 0; i-- {
    go func() {
        if i%2 == 0 ...  // захватывает переменную i
    }()
}

Все горутины захватывают одну переменную i, которая меняется.

Проблема 2: Data Race на переменной maxNum

var maxNum int
// Несколько горутин одновременно читают и пишут в maxNum БЕЗ синхронизации
if i > maxNum {      // race: чтение
    maxNum = i       // race: запись
}

Проблема 3: Отсутствует WaitGroup

for i := 1000; i > 0; i-- {
    go func() { ... }()
}
fmt.Printf("Maximum is %d", maxNum)  // главная горутина не ждёт!

Главная горутина выводит результат, прежде чем все background горутины завершатся.

2. Почему результат неожиданный?

Читать полностью ->
Найти баг: Data Race в map
2.3 Middle🔥 22💬 1

Решение

Это классическая задача на обнаружение data race. Go имеет встроенный инструмент для обнаружения именно таких проблем.

Ответ на вопросы

1. Какая проблема в этом коде?

Проблема: Data Race (race condition)

Мапа в Go НЕ потокобезопасна. Несколько горутин одновременно пишут в одну и ту же map без синхронизации.

Горотина 1: m[10] = 100
Горотина 2: m[20] = 400  ← одновременно!
Горотина 3: m[30] = 900
Горотина 4: m[40] = 1600

Внутри Go это вызывает проблемы:

  • Потеря данных (некоторые записи теряются)
  • Panic (может быть рассогласование внутренних структур map)
  • Undefined behavior (непредсказуемые результаты)

Как обнаружить data race?

Запустить с флагом -race:

go run -race main.go
# ИЛИ
go test -race ./...

Вывод:

==================
WARNING: DATA RACE
Write at 0x00c0001da000 by goroutine 8:
    main.main.func1()
        /path/to/main.go:15 +0x44
Читать полностью ->
Проверка скобок
2.0 Middle🔥 21💬 1

Решение

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

Код

func isValid(s string) bool {
    stack := make([]rune, 0)
    pairs := map[rune]rune{')': '(', '}': '{', ']': '['}
    open := map[rune]bool{'(': true, '{': true, '[': true}
    
    for _, ch := range s {
        if open[ch] {
            stack = append(stack, ch)
        } else if len(stack) == 0 || stack[len(stack)-1] != pairs[ch] {
            return false
        } else {
            stack = stack[:len(stack)-1]
        }
    }
    
    return len(stack) == 0
}

Как работает

  1. Открывающие скобки добавляем в стек
  2. Для закрывающих скобок проверяем соответствие вершине стека
  3. Если совпадает - удаляем из стека
  4. В конце стек должен быть пуст

Сложность

  • Время: O(n) - один проход
  • Память: O(n) - максимум все открывающие в стеке

Примеры

Читать полностью ->
HTTP клиент к внешнему сервису с батчами
2.0 Middle🔥 21💬 1

HTTP клиент с батчами - реальное решение KazanExpress

Описание задачи

Нужно реализовать батч-клиент, который:

  • Накапливает запросы и отправляет их группами (батчами)
  • Соблюдает ограничение n элементов за период p
  • Не блокирует отправителей
  • Поддерживает graceful shutdown через context
  • Гарантирует доставку данных

Это реальная задача для работы с rate-limited API (Telegram, Yandex, 1C, etc.)

Архитектурное решение

Основные компоненты:

  1. Входящий канал - для получения элементов от клиентов
  2. Горутина-батчер - накапливает элементы и отправляет батчи
  3. Таймер - гарантирует отправку даже если батч не полный
  4. Результаты - каналы для получения результатов/ошибок

Реализация

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)
Читать полностью ->
Простой HTTP сервер
1.0 Junior🔥 20💬 1

Простой HTTP сервер

Описание задачи

Реализовать HTTP сервер с:

  • GET /health - проверка статуса
  • GET /users/:id - получение пользователя
  • POST /users - создание пользователя
  • Логирование запросов
  • X-Request-ID middleware

Архитектура

Используем стандартную net/http с несколькими подходами:

  1. Стандартный http.ServeMux
  2. Или chi/gin для красивого routing

Компоненты

  1. User структура с ID, Name, Email
  2. In-memory хранилище (map)
  3. Обработчики для каждого эндпоинта
  4. Middleware для логирования и X-Request-ID
  5. JSON кодирование/декодирование

GET /health

Просто возвращаем status 200 и JSON: {"status": "ok"}

GET /users/:id

Парсим ID из URL Ищем пользователя в хранилище Возвращаем 200 с JSON или 404

POST /users

Парсим JSON из body Валидируем Name и Email Создаем новый User с ID = max_id + 1 Возвращаем 201 с созданным пользователем

Логирование

Мидлвэр который:

  • Логирует метод и путь
  • Логирует время выполнения
  • Логирует код ответа
Читать полностью ->
Ограниченное хранение данных в map
1.6 Junior🔥 20💬 1

Ограниченное хранилище (FIFO Map)

Описание задачи

Мап с максимальным размером где при превышении лимита удаляется самый старый элемент (FIFO - First In First Out).

Отличие от LRU:

  • LRU: Get перемещает элемент в конец (свежий)
  • FIFO: Get не влияет на порядок, только время добавления

Архитектура

Используем две структуры:

  1. map[string]interface{} - для быстрого доступа O(1)
  2. []string - очередь ключей в порядке добавления

Это дешевле чем двусвязный список как в LRU.

Реализация FIFO

Непосредственно используем:

  • Mutex для синхронизации
  • map для быстрого Get/Set
  • slice для отслеживания порядка

Алгоритм Set:

  1. Если key уже существует: обновляем значение, порядок не меняем
  2. Если key новый: a. Если длина меньше maxSize: добавляем в конец queue b. Если длина равна maxSize: удаляем первый элемент из map и queue, добавляем новый
  3. Обновляем значение в map

Алгоритм Get:

  1. Простой lookup в map
  2. Возвращаем значение и флаг
  3. Порядок не меняется
Читать полностью ->
FizzBuzz
1.8 Middle🔥 20💬 1

FizzBuzz - полное решение

Описание задачи

FizzBuzz - классическая задача на интервью, которая проверяет базовые навыки:

  • Работа с циклами
  • Условные операторы
  • Строковые операции
  • Построение срезов

Несмотря на простоту, задача часто обнаруживает недостатки в кодировании.

Решение 1: Базовое (если-то-иначе)

package main

import (
    "fmt"
    "strconv"
)

func fizzBuzz(n int) []string {
    result := make([]string, n)
    
    for i := 1; i <= n; i++ {
        if i%3 == 0 && i%5 == 0 {
            // Делится и на 3, и на 5
            result[i-1] = "FizzBuzz"
        } else if i%3 == 0 {
            // Делится только на 3
            result[i-1] = "Fizz"
        } else if i%5 == 0 {
            // Делится только на 5
            result[i-1] = "Buzz"
        } else {
            // Ни на что не делится
            result[i-1] = strconv.Itoa(i)
        }
    }
    
    return result
}
Читать полностью ->
Min и Max для слайса
2.0 Middle🔥 20💬 1

Решение

Анализ задачи

Требования:

  • Найти минимальное и максимальное значения в слайсе
  • Обработать пустой слайс (вернуть ошибку)
  • Сложность O(n) — однопроходный алгоритм

Простое решение: пройти по слайсу один раз, сравнивая элементы.

Решение

func Min(nums []int) (int, error) {
    // Проверка на пустой слайс
    if len(nums) == 0 {
        return 0, errors.New("empty slice")
    }
    
    min := nums[0]
    for i := 1; i < len(nums); i++ {
        if nums[i] < min {
            min = nums[i]
        }
    }
    
    return min, nil
}

func Max(nums []int) (int, error) {
    // Проверка на пустой слайс
    if len(nums) == 0 {
        return 0, errors.New("empty slice")
    }
    
    max := nums[0]
    for i := 1; i < len(nums); i++ {
        if nums[i] > max {
            max = nums[i]
        }
    }
    
    return max, nil
}

Пояснения реализации

Читать полностью ->
Таймаут для горутины
1.2 Junior🔥 20💬 1

Решение

Анализ задачи

Требования:

  • Выполнить задачу в отдельной горутине с ограничением по времени
  • Вернуть результат, если задача завершилась до таймаута
  • Вернуть ошибку, если время истекло
  • Использовать select и time.After для управления таймаутом

Ключевой паттерн: select с двумя каналами — один для результата, один для таймаута.

Решение

Читать полностью ->
Реализовать бинарный поиск
1.0 Junior🔥 20💬 1

Решение

Бинарный поиск — это фундаментальный алгоритм поиска в отсортированном массиве. Основная идея: разделить пространство поиска пополам на каждой итерации.

Подход

  1. Инициализируем два указателя: left = 0, right = len(arr) - 1
  2. Пока left <= right:
    • Вычисляем середину: mid = (left + right) / 2
    • Если arr[mid] == target → возвращаем mid
    • Если arr[mid] < target → ищем в правой половине (left = mid + 1)
    • Если arr[mid] > target → ищем в левой половине (right = mid - 1)
  3. Если цикл завершился, элемент не найден → возвращаем -1

Реализация

Читать полностью ->
Реализовать strstr (поиск подстроки)
1.0 Junior🔥 19💬 1

Поиск подстроки

Задача: найти первое вхождение needle в haystack.

Алгоритм Brute Force

Шаг 1: Если needle пустая, return 0 Шаг 2: Если needle длиннее haystack, return -1 Шаг 3: Для каждой позиции i в haystack (от 0 до len-len(needle)): Проверяем совпадает ли подстрока haystack[i:i+len(needle)] с needle Если совпадает, return i Шаг 4: Если не найдена, return -1

Пример

haystack = hello, needle = ll

Позиция 0: haystack[0:2] = he != ll Позиция 1: haystack[1:3] = el != ll Позиция 2: haystack[2:4] = ll == ll -> return 2

Сложность

Time: O(n*m) где n=len(haystack), m=len(needle) Space: O(1)

Граничные случаи

  1. needle = пусто -> return 0
  2. haystack = пусто, needle не пусто -> return -1
  3. needle == haystack -> return 0
  4. needle в конце haystack -> работает правильно
  5. needle не найдена -> return -1

Оптимизация

Для очень больших строк использовать KMP или Rabin-Karp алгоритмы с O(n+m) временем.

Читать полностью ->
Числа Фибоначчи с мемоизацией
1.8 Middle🔥 19💬 1

Решение

Реализуем функцию вычисления чисел Фибоначчи с мемоизацией.

Простая версия с кэшем

func fibonacci(n int) int {
    cache := make(map[int]int)
    
    var fib func(int) int
    fib = func(x int) int {
        if x <= 1 {
            return x
        }
        
        if val, ok := cache[x]; ok {
            return val
        }
        
        result := fib(x-1) + fib(x-2)
        cache[x] = result
        return result
    }
    
    return fib(n)
}

Потокобезопасная версия с sync.Map

var fibCache sync.Map

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    
    val, ok := fibCache.Load(n)
    if ok {
        return val.(int)
    }
    
    result := fibonacci(n-1) + fibonacci(n-2)
    fibCache.Store(n, result)
    return result
}

Быстрая итеративная версия

Читать полностью ->
Найти баг: deadlock в select
1.8 Middle🔥 19💬 1

Решение: Найти баг — deadlock в select

Проблема 1: Бесконечный select без default

Основная проблема — бесконечный for цикл с select:

for {
    select {
    case q := <-ch:
        fmt.Println(q)
    }
}

Этот код никогда не завершится, потому что:

  • Все 5 горутин выполнены и заблокированы в mu.Lock()
  • Основной поток вечно ждёт данные из ch
  • wg.Wait() никогда не достигается

Deadlock: основной поток ждёт горутины, но горутины не могут завершиться (ждут отправки в канал, который основной поток не успевает читать), а основной поток не может закончить читать, потому что for бесконечен.

Проблема 2: Неправильная логика синхронизации

Все горутины конкурируют за один mu.Mutex, но в буферизированном канале нет необходимости синхронизировать отправку. Мьютекс создаёт дополнительные задержки и усложняет диагностику проблем.

Исправленный вариант 1: С сигналом завершения

Читать полностью ->
Развернуть слова в строке
2.2 Middle🔥 18💬 1

Решение

Min и Max — это базовые операции для работы со слайсами. Go в отличие от Python/JavaScript не имеет встроенных функций, поэтому нужно реализовать самостоятельно.

Подход

  1. Проверить, что слайс не пустой
  2. Инициализировать результат первым элементом
  3. Пройти по остальным элементам, сравнивая

Реализация

package main

import (
    "errors"
    "fmt"
)

var ErrEmptySlice = errors.New("empty slice")

func Min(nums []int) (int, error) {
    if len(nums) == 0 {
        return 0, ErrEmptySlice
    }
    
    min := nums[0]
    for i := 1; i < len(nums); i++ {
        if nums[i] < min {
            min = nums[i]
        }
    }
    
    return min, nil
}

func Max(nums []int) (int, error) {
    if len(nums) == 0 {
        return 0, ErrEmptySlice
    }
    
    max := nums[0]
    for i := 1; i < len(nums); i++ {
        if nums[i] > max {
            max = nums[i]
        }
    }
    
    return max, nil
}
Читать полностью ->
Two Sum
2.0 Middle🔥 18💬 1

Решение Two Sum

Описание задачи

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

Подход с хеш-таблицей (O(n) по времени)

Оптимальное решение использует хеш-таблицу (map в Go) для хранения пройденных чисел и их индексов. Идея простая:

  1. Проходим по массиву один раз
  2. Для каждого элемента проверяем, есть ли уже в map число, которое дополнит текущий элемент до target
  3. Если есть — возвращаем индексы
  4. Если нет — добавляем текущее число в map и продолжаем

Время: O(n) — один проход по массиву. Пространство: O(n) — размер map в худшем случае.

Реализация

Читать полностью ->
Что выведет код? Range и указатели
2.3 Middle🔥 18💬 1

Решение

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

Ответ на вопросы

1. Что выведет программа?

Программа может вывести:

4
4
4
4

ИЛИ какую-то комбинацию вроде 2 3 4 4, но скорее всего 4 4 4 4.

НЕ выведет 0, 1, 2, 3 — это самое важное!

2. Почему результат неожиданный?

Проблема в классической ошибке с closures и переменными цикла:

Анализ кода

for i := 0; i < 4; i++ {        // i = 0, 1, 2, 3
    go func() {
        ch <- &i              // ключевая ошибка!
    }()
}

Что происходит:

  1. Итерация 1: i = 0

    • Создаётся горутина, которая захватывает ссылку на переменную i (не значение!)
    • &i — это адрес переменной i
  2. Итерация 2: i = 1

    • Переменная i изменяется на 1
    • Первая горутина всё ещё в очереди, но когда она выполнится, i будет 1 или больше
  3. Итерация 3: i = 2

    • i становится 2
Читать полностью ->
Что выведет код? Interface и nil
1.8 Middle🔥 18💬 1

Решение

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

Ответ

ERROR

Программа выведет ERROR, хотя p явно равна nil и if false не выполняется.

Объяснение: nil в interface{} и error

Проблема в тонкой разнице между типизированным nil и истинным nil.

Как Go представляет interface

Каждый interface в Go — это пара (type, value):

interface{} = (type, value)

Когда вы присваиваете значение interface:

var p *MyError = nil              // это nil указатель
err := error(p)                   // err = (*MyError, nil)

Важно: err НЕ равен nil в смысле interface! Это типизированный nil.

err = (*MyError, nil)  // type = *MyError, value = nil

Сравнение err != nil проверяет не value, а всю пару:

if err != nil {  // проверяет: тип не nil И value не nil
    // true, если обе части ненулевые
}
Читать полностью ->
Worker Pool
2.0 Middle🔥 18💬 1

Решение

Worker Pool — это паттерн параллельной обработки, где фиксированное число горутин-воркеров обрабатывают задачи из общей очереди. Это позволяет контролировать использование ресурсов и избежать создания неограниченного количества горутин.

Подход

  1. Запускаем numWorkers горутин (воркеров)
  2. Каждый воркер в цикле читает задачу из канала jobs
  3. Обрабатывает задачу (в примере — возводит число в квадрат)
  4. Отправляет результат в канал results
  5. При закрытии jobs воркер корректно завершается

Реализация

import "sync"
Читать полностью ->
Первый результат выигрывает
2.0 Middle🔥 17💬 1

Решение: Первый результат выигрывает

Основная идея

Паттерн "first wins" часто используется в production для:

  • Гонки между несколькими источниками данных (кеши, БД, API)
  • Ускорение обработки: берём результат первого
  • Failover: если одно не работает, другое сработает

Используем канал результатов и select для получения первого значения.

Простая реализация

import (
    "context"
    "fmt"
)

type result struct {
    value int
    err   error
}
Читать полностью ->
Singleton паттерн в Go
1.0 Junior🔥 16💬 1

Решение

Singleton паттерн — создание только одного экземпляра объекта с потокобезопасным доступом.

Решение 1: sync.Once (РЕКОМЕНДУЕТСЯ)

var (
    instance *Database
    once     sync.Once
)

func GetInstance() *Database {
    once.Do(func() {
        instance = &Database{
            connection: "default-connection",
        }
    })
    return instance
}

Преимущества sync.Once:

  • Выполняет функцию ровно один раз при конкурентном доступе
  • Минимальный overhead (проверка без lock после первого вызова)
  • Стандартное решение в Go сообществе
  • Самое простое и надёжное

Решение 2: sync.Mutex (явное управление)

type databaseHolder struct {
    mu       sync.Mutex
    instance *Database
}

var holder = &databaseHolder{}
Читать полностью ->
Что выведет код? Закрытый канал
2.0 Middle🔥 16💬 1

Решение: Что выведет код с закрытым каналом

Ответ на вопрос 1

Программа выведет:

val=1, ok=true
val=2, ok=true
val=3, ok=true
val=0, ok=false
val=0, ok=false

Почему такой результат?

Шаг 1: Создание и заполнение канала

ch := make(chan int, 3)  // Буферизированный канал на 3 элемента
ch <- 1  // Элемент 1 в буфер
ch <- 2  // Элемент 2 в буфер
ch <- 3  // Элемент 3 в буфер
// Буфер полон: [1, 2, 3]

Шаг 2: Закрытие канала

close(ch)  // Канал закрыт, но элементы остаются в буфере

Шаг 3: Первые три итерации цикла

i=0: val, ok := <-ch  // Читаем 1 из буфера
     ok=true (элемент найден)
     fmt.Printf("val=1, ok=true\n")

i=1: val, ok := <-ch  // Читаем 2 из буфера
     ok=true (элемент найден)
     fmt.Printf("val=2, ok=true\n")

i=2: val, ok := <-ch  // Читаем 3 из буфера
     ok=true (элемент найден)
     fmt.Printf("val=3, ok=true\n")
Читать полностью ->
Объединение отсортированных слайсов
2.0 Middle🔥 16💬 1

Решение

Reverse words in string — это классическая задача на манипуляцию строками. Нужно развернуть порядок слов, сохраняя сами слова неизменёнными.

Подход

  1. Разбить строку на слова (по пробелам)
  2. Развернуть слайс слов
  3. Объединить обратно в строку

Реализация

package main

import (
    "fmt"
    "strings"
)

func reverseWords(s string) string {
    // Разбить на слова
    words := strings.Fields(s)  // автоматически удаляет пустые строки
    
    // Развернуть слайс
    for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
        words[i], words[j] = words[j], words[i]
    }
    
    // Объединить обратно
    return strings.Join(words, " ")
}
Читать полностью ->
Все перестановки строки
2.2 Middle🔥 16💬 1

Решение: Все перестановки строки

Основная идея

Используем рекурсию с backtracking:

  1. Для каждой позиции пробуем все символы
  2. Берём символ, рекурсивно генерируем перестановки остальных
  3. Возвращаемся назад (backtrack) и пробуем следующий

Рекурсивная реализация (наиболее эффективная)

func permutations(s string) []string {
    var result []string
    permute([]rune(s), 0, &result)
    return result
}
Читать полностью ->
Что выведет код? Map и итерация
1.0 Junior🔥 15💬 1

Решение: Map и итерация в Go

Ответ на вопрос 1

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

Каждый запуск программы может вывести значения в разном порядке:

Запуск 1:

1: one
2: two
3: three

Запуск 2:

3: three
1: one
2: two

Запуск 3:

2: two
3: three
1: one

Ответ на вопрос 2: Почему нет гарантии порядка?

Go намеренно рандомизирует порядок итерации по map в целях безопасности и стимулирования правильного кода. Вот причины:

1. Реализация: Hash table

Map в Go реализована как hash table:
- Ключ преобразуется в hash через hash function
- Hash определяет бакет (позицию в массиве)
- Порядок итерации зависит от h
легких значений

2. Причины рандомизации:

Читать полностью ->
Самая длинная подстрока без повторяющихся символов
1.8 Middle🔥 15💬 1

Решение: Самая длинная подстрока без повторяющихся символов

Основная идея: Sliding Window

Используем два указателя (левый и правый):

  1. Правый указатель движется вперёд, добавляя символы
  2. Если встретили повторение, движем левый указатель
  3. Отслеживаем максимальную длину

Оптимальная реализация O(n)

Читать полностью ->
Что выведет код? Slice и изменение в функции
2.0 Middle🔥 15💬 1

Решение: Что выведет код со слайсами

Ответ на вопрос 1

Программа выведет:

[1 10 3 40]

Почему именно этот результат?

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

Что такое слайс в Go

Слайс — это не массив, а структура, содержащая три поля:

type slice struct {
    data *T      // указатель на базовый массив
    len  int     // текущая длина
    cap  int     // ёмкость
}

Это очень важно: слайс передаётся по значению, но указатель на данные остаётся тем же!

Пошаговое выполнение

Инициализация:

s := []int{1, 2, 3}
// s содержит:
// data: [1, 2, 3]
// len: 3
// cap: 3

Вызов f2(&s) — передаём указатель на слайс:

func f2(s *[]int) {
    (*s)[1] = 10  // меняем элемент базового массива
    *s = append(*s, 40)  // расширяем сам слайс
}
  • (*s)[1] = 10 — разыменовываем указатель и меняем элемент
    • data[1] становится 10
    • s теперь [1, 10, 3]
Читать полностью ->
Генератор слайса уникальных чисел
1.6 Junior🔥 15💬 1

Решение

Анализ задачи

Требования:

  • Генерировать n уникальных случайных чисел в диапазоне [min, max]
  • Если невозможно выбрать n уникальных чисел, вернуть ошибку
  • Обрабатывать граничные случаи

Сложность: нужно избежать бесконечного цикла, если n больше количества доступных чисел.

Решение 1: Использование map для отслеживания

Читать полностью ->
Калькулятор (тестовое задание)
1.0 Junior🔥 14💬 1

Калькулятор - реальное тестовое от Kata Academy

Описание задачи

Реализовать калькулятор который:

  • Парсит арифметическое выражение
  • Поддерживает +, -, *, / с правильным приоритетом
  • Работает с араб 1-10 и римскими цифрами
  • При делении римских - результат вниз
  • Корректная обработка ошибок

Решение базовое (арабские цифры)

Алгоритм:

  1. Split выражение по пробелам
  2. Валидируем формат: число оператор число
  3. Вычисляем результат
  4. Обрабатываем ошибки (деление на 0, неверные числа)

Приоритет операций:

  • Сначала * и /
  • Потом + и -

Примеры: 8 / 2 + 3 = 4 + 3 = 7 2 + 2 * 2 = 2 + 4 = 6

Решение полное (с римскими)

Шаг 1: Определяем тип чисел (арабские или римские)

Шаг 2: Конвертируем в целые числа

  • Арабские: strconv.Atoi
  • Римские: ParseRoman

Шаг 3: Вычисляем результат

Шаг 4: Конвертируем результат обратно

  • Если были арабские: выводим число
  • Если римские: ConvertToRoman

Проверки при парсинге

Читать полностью ->
Pub/Sub система
2.3 Middle🔥 14💬 1

Pub/Sub система

Построим систему publish/subscribe с каналами.

Реализация

Основные компоненты:

  • RWMutex для синхронизации
  • map[string][]chan string для подписчиков по темам
  • Buffered каналы для неблокирующей отправки

Алгоритм Subscribe:

  1. Создаем новый канал размера 10
  2. Добавляем в список подписчиков темы
  3. Возвращаем канал клиенту

Алгоритм Publish:

  1. Получаем список подписчиков для темы
  2. Отправляем сообщение каждому с таймаутом
  3. Пропускаем медленных подписчиков

Алгоритм Unsubscribe:

  1. Ищем канал в списке подписчиков
  2. Удаляем из списка
  3. Закрываем канал

Алгоритм Close:

  1. Закрываем все каналы во всех темах
  2. Очищаем структуру данных

Сложность

  • Subscribe: O(1) - добавление в конец массива
  • Publish: O(n) где n - количество подписчиков
  • Unsubscribe: O(n) - поиск в массиве
  • Close: O(n*m) где n темы, m подписчики

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

Buffered каналы: каждый канал размера 10 позволяет избежать блокировки издателя при отправке 10 сообщений

Читать полностью ->
Swap без временной переменной
2.0 Middle🔥 14💬 1

Решение

Анализ задачи

Требования:

  • Функция должна менять местами значения двух целых чисел
  • Нельзя использовать дополнительную переменную
  • Значения передаются по указателям

Бонус: несколько подходов — арифметический, побитовый XOR и идиоматический Go.

Решение 1: Арифметический способ

func swapArithmetic(a, b *int) {
    *a = *a + *b
    *b = *a - *b
    *a = *a - *b
}

Как это работает:

  • Шаг 1: a = a + b (теперь a содержит сумму)
  • Шаг 2: b = (a + b) - b = a (b получает старое значение a)
  • Шаг 3: a = (a + b) - (a + b) - b = b (a получает старое значение b)

Пример: a=5, b=10

  • Шаг 1: a = 5 + 10 = 15
  • Шаг 2: b = 15 - 10 = 5
  • Шаг 3: a = 15 - 5 = 10

Минусы: может привести к переполнению для больших чисел.

Решение 2: XOR (побитовое исключающее ИЛИ)

func swapXOR(a, b *int) {
    *a = *a ^ *b
    *b = *a ^ *b
    *a = *a ^ *b
}
Читать полностью ->
Генератор случайных чисел
2.0 Middle🔥 14💬 1

Решение: Генератор случайных чисел

Основная идея

Используем подход "Fisher-Yates shuffle" (также известный как "Knuth shuffle"):

  1. Создаём слайс с числами от min до max
  2. Тасуем (shuffle) случайно
  3. Отправляем числа в канал
  4. Закрываем канал

Простая реализация

import (
    "math/rand"
)

func UniqueRandomGenerator(min, max int) <-chan int {
    ch := make(chan int)
    
    go func() {
        defer close(ch)
        
        // Создаём слайс чисел [min, max]
        numbers := make([]int, max-min+1)
        for i := 0; i < len(numbers); i++ {
            numbers[i] = min + i
        }
        
        // Fisher-Yates shuffle
        for i := len(numbers) - 1; i > 0; i-- {
            j := rand.Intn(i + 1)
            numbers[i], numbers[j] = numbers[j], numbers[i]
        }
        
        // Отправляем числа в канал
        for _, num := range numbers {
            ch <- num
        }
    }()
    
    return ch
}
Читать полностью ->
Реализовать Queue на слайсе
2.0 Middle🔥 14💬 1

Решение: Реализовать Queue на слайсе

Основная идея

Queue (очередь) — структура данных FIFO (First In, First Out). Элементы добавляются с конца и удаляются с начала. На слайсе это неэффективно для каждого Dequeue(), так как нужно смещать все элементы. Однако для учебных целей можно использовать простой подход или оптимизированный с индексами.

Наивный подход: с копированием

type Queue struct {
    items []int
}

func NewQueue() *Queue {
    return &Queue{
        items: make([]int, 0),
    }
}

func (q *Queue) Enqueue(val int) {
    q.items = append(q.items, val)
}

func (q *Queue) Dequeue() (int, bool) {
    if len(q.items) == 0 {
        return 0, false
    }
    val := q.items[0]
    q.items = q.items[1:]
    return val, true
}

func (q *Queue) Front() (int, bool) {
    if len(q.items) == 0 {
        return 0, false
    }
    return q.items[0], true
}

func (q *Queue) IsEmpty() bool {
    return len(q.items) == 0
}
Читать полностью ->
Объединение данных из нескольких map
1.0 Junior🔥 13💬 1

Объединение map

Задача: объединить несколько map, суммируя значения при совпадении ключей.

Алгоритм

  1. Создаем пустую результирующую map
  2. Для каждой входной map:
    • Для каждой пары ключ-значение:
      • Если ключ существует: добавляем значение
      • Если ключа нет: устанавливаем значение
  3. Возвращаем результат

Реализация

Создаем map результатом. Итерируем по всем входным map. Для каждого ключа суммируем значения используя +=.

Cложность:

  • Time: O(n) где n - всего элементов
  • Space: O(m) где m - уникальные ключи

Особенности

  1. result[key] += value работает правильно даже если ключа нет (zero value = 0)
  2. Входные map не изменяются
  3. Порядок элементов в map не гарантирован
  4. Можно передать zero maps

Примеры

m1 = {a:1, b:2} m2 = {b:3, c:4} m3 = {a:5}

Читать полностью ->
Параллельное чтение файла по частям
1.2 Junior🔥 13💬 1

Параллельное чтение файла

Описание задачи

Обрабатывать большой файл параллельно:

  • Разбить на chunks фиксированного размера
  • Обрабатывать в worker pool
  • Ограничить одновременные горутины
  • Обрабатывать ошибки
  • Поддержка context отмены

Архитектура решения

Используем паттерн worker pool:

  1. Стартуем N worker горутин
  2. Читаем файл в main горутине
  3. Отправляем chunks в канал
  4. Workers обрабатывают chunks
  5. Собираем ошибки и возвращаем

Параметры

  • chunkSize: 8KB (можно 4KB до 64KB)
  • workers: количество одновременных обработчиков
  • timeout: контекст для отмены

Алгоритм

Шаг 1: Открыть файл Шаг 2: Создать буфер размера chunkSize Шаг 3: Запустить workers Шаг 4: В цикле:

  • Читать chunk из файла
  • Отправить в канал workers
  • Проверить context.Done() Шаг 5: Закрыть канал chunks Шаг 6: Дождаться завершения workers Шаг 7: Проверить ошибки

Обработка ошибок

Читать полностью ->
LRU Cache
2.0 Middle🔥 13💬 1

LRU Cache - полное решение

Описание задачи

LRU (Least Recently Used) Cache - это кэширующая структура данных с фиксированной ёмкостью, которая автоматически вытесняет наименее недавно использованный элемент при переполнении. Это часто используется для оптимизации доступа к часто используемым данным.

Архитектурное решение

Для достижения O(1) сложности для обеих операций используем комбинацию двух структур данных:

  1. Двусвязный список (doubly-linked list) - для хранения элементов в порядке использования
  2. *HashMap (map[int]Node) - для быстрого поиска элемента за O(1)

Двусвязный список позволяет за O(1) переместить элемент в конец (самый свежий) и удалить элемент из начала (самый старый).

Реализация

package main

import "fmt"

type Node struct {
    key, value int
    prev, next *Node
}
Читать полностью ->
Пересечение двух слайсов
1.6 Junior🔥 13💬 1

Решение

Задача требует найти элементы, которые присутствуют одновременно в обоих слайсах. Ключевой момент — входные данные неупорядочены, поэтому прямое сравнение неэффективно.

Подход

Используем map (хеш-таблицу) для оптимизации:

  1. Создаём map из элементов первого слайса
  2. Итерируем по второму слайсу и ищем совпадения
  3. Добавляем найденные элементы в результат (учитывая дубликаты)

Реализация

Читать полностью ->