Перейти к основному содержимому

Я ПРОШЁЛ СОБЕС В VK С ПОМОЩЬЮ ИИ / ТЕХНИЧЕСКОЕ ИНТЕРВЬЮ НА GO-РАЗРАБОТЧИКА (ПЕРВЫЙ ЭТАП)

· 136 мин. чтения

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

Вопрос 1. Реализовать функцию, которая находит максимальное расстояние до ближайшего зрителя в кинотеатре, где массив содержит 0 (свободно) и 1 (занято).

Таймкод: 00:02:07

Ответ собеседника: Неполный. Предложен алгоритм с одним проходом по массиву. Для нулей до первой единицы расстояние равно индексу. Между единицами берётся середина (разница индексов / 2). Для нулей после последней единицы — длина массива минус позиция последней единицы минус 1. Код написан на Go, при первом запуске обнаружена ошибка. После исправления на тесте [0,0,0,0,1] получен правильный результат 4. Сложность O(n) по времени, O(1) по памяти.

Правильный ответ:

Задача сводится к нахождению максимального расстояния до ближайшей единицы в бинарном массиве. Алгоритм действительно работает за O(n) времени и O(1) дополнительной памяти.

Ключевые моменты реализации:

  1. Левая граница: Для нулей до первой единицы расстояние равно индексу этого нуля (например, [0,0,0,0,1] → расстояние 4).
  2. Правая граница: Для нулей после последней единицы расстояние равно len(seats) - 1 - lastOne (например, [1,0,0,0,0] → расстояние 4).
  3. Между единицами: Для последовательности нулей между двумя единицами оптимальное место — середина, расстояние = (right - left) / 2.

Корректная реализация на Go:

package main

import (
"fmt"
"math"
)

func maxDistToClosest(seats []int) int {
n := len(seats)
maxDist := 0

// Находим первую единицу
firstOne := -1
for i := 0; i < n; i++ {
if seats[i] == 1 {
firstOne = i
break
}
}

// Если все места пустые (граничный случай)
if firstOne == -1 {
return n - 1 // садимся на край
}

// Расстояние до первой единицы (левая граница)
maxDist = firstOne

lastOne := firstOne
for i := firstOne + 1; i < n; i++ {
if seats[i] == 1 {
// Расстояние между двумя единицами
dist := (i - lastOne) / 2
if dist > maxDist {
maxDist = dist
}
lastOne = i
}
}

// Расстояние от последней единицы до конца (правая граница)
rightDist := n - 1 - lastOne
if rightDist > maxDist {
maxDist = rightDist
}

return maxDist
}

Тесткейсы для проверки:

func main() {
// Тест 1: Левая граница
fmt.Println(maxDistToClosest([]int{0, 0, 0, 0, 1})) // 4

// Тест 2: Правая граница
fmt.Println(maxDistToClosest([]int{1, 0, 0, 0, 0})) // 4

// Тест 3: Между единицами
fmt.Println(maxDistToClosest([]int{1, 0, 0, 0, 1})) // 2

// Тест 4: Сложный случай
fmt.Println(maxDistToClosest([]int{1, 0, 0, 1, 0, 1})) // 2

// Тест 5: Все заняты
fmt.Println(maxDistToClosest([]int{1, 1, 1, 1})) // 0

// Тест 6: Одно место
fmt.Println(maxDistToClosest([]int{0})) // 0
fmt.Println(maxDistToClosest([]int{1})) // 0
}

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

  • Пустой массив или массив из одного элемента
  • Все места заняты (нет свободных)
  • Все места свободны (нет зрителей)
  • Только одно занятое место

Улучшенная версия с одним проходом:

func maxDistToClosest(seats []int) int {
n := len(seats)
maxDist := 0
lastOne := -1

for i := 0; i < n; i++ {
if seats[i] == 1 {
if lastOne == -1 {
// Первая единица - проверяем левую границу
maxDist = i
} else {
// Между единицами
dist := (i - lastOne) / 2
if dist > maxDist {
maxDist = dist
}
}
lastOne = i
}
}

// Правая граница
rightDist := n - 1 - lastOne
if rightDist > maxDist {
maxDist = rightDist
}

return maxDist
}

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

Вопрос 2. Нужно ли после основного цикла делать дополнительную проверку для нулей в конце массива в задаче о местах в кинотеатре?

Таймкод: 00:06:44

Ответ собеседника: Неполный. Упомянут граничный случай с нулями в конце массива, но конкретная реализация этого случая в коде не показана. Собеседник только обозначил проблему, но не реализовал проверку после цикла.

Правильный ответ:

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

Почему это необходимо:

Основной цикл обрабатывает только ситуации, когда встречается единица. Если массив заканчивается нулями (например, [1, 0, 0, 0, 0]), то последняя последовательность нулей никогда не будет обработана внутри цикла, потому что нет единицы, которая бы "замкнула" этот участок.

Примеры, где без проверки после цикл будет ошибка:

// Тест 1: Нули в конце
seats := []int{1, 0, 0, 0, 0}
// Ожидаемый результат: 4 (садимся на последнее место)
// Без проверки после цикла: результат будет 0 или неверный

// Тест 2: Нули в начале и конце
seats := []int{0, 0, 1, 0, 0, 0}
// Ожидаемый результат: 3 (садимся на последнее место)

Корректная реализация с проверкой после цикла:

func maxDistToClosest(seats []int) int {
n := len(seats)
maxDist := 0
lastOne := -1

for i := 0; i < n; i++ {
if seats[i] == 1 {
if lastOne == -1 {
// Первая единица - левая граница
maxDist = i
} else {
// Между двумя единицами
dist := (i - lastOne) / 2
if dist > maxDist {
maxDist = dist
}
}
lastOne = i
}
}

// !!! КРИТИЧЕСКИ ВАЖНАЯ ПРОВЕРКА ПОСЛЕ ЦИКЛА !!!
// Обрабатываем нули после последней единицы
if lastOne != -1 {
rightDist := n - 1 - lastOne
if rightDist > maxDist {
maxDist = rightDist
}
}

return maxDist
}

Подробный разбор логики:

1. Внутри цикла мы обрабатываем:

  • Левую границу (от начала до первой единицы)
  • Промежутки между единицами

2. После цикла мы обрабатываем:

  • Правую границу (от последней единицы до конца массива)

Вычисление расстояния для правой границы:

Формула: rightDist = n - 1 - lastOne

Пример: seats = [1, 0, 0, 0, 0]
- n = 5
- lastOne = 0 (индекс первой и единственной единицы)
- rightDist = 5 - 1 - 0 = 4

Демонстрация ошибки без проверки:

// Неправильная версия (без проверки после цикла)
func maxDistToClosestBroken(seats []int) int {
n := len(seats)
maxDist := 0
lastOne := -1

for i := 0; i < n; i++ {
if seats[i] == 1 {
if lastOne == -1 {
maxDist = i
} else {
dist := (i - lastOne) / 2
if dist > maxDist {
maxDist = dist
}
}
lastOne = i
}
}

// ОТСУТСТВУЕТ ПРОВЕРКА ПРАВОЙ ГРАНИЦЫ!
return maxDist
}

// Тесты:
// [1, 0, 0, 0, 0] → вернёт 0 вместо 4 (ОШИБКА!)
// [0, 0, 1, 0, 0, 0] → вернёт 2 вместо 3 (ОШИБКА!)

Полный набор тестов для проверки:

func main() {
tests := []struct {
seats []int
expected int
}{
{[]int{1, 0, 0, 0, 1, 0, 1}, 2},
{[]int{1, 0, 0, 0}, 3}, // Нули в конце
{[]int{0, 0, 0, 1}, 3}, // Нули в начале
{[]int{0, 0, 1, 0, 0, 0}, 3}, // Нули с обеих сторон
{[]int{1, 0, 1}, 1}, // Один ноль между
{[]int{1, 1, 1, 1}, 0}, // Все заняты
}

for _, test := range tests {
result := maxDistToClosest(test.seats)
if result != test.expected {
fmt.Printf("FAIL: seats=%v, got=%d, expected=%d\n",
test.seats, result, test.expected)
} else {
fmt.Printf("PASS: seats=%v, result=%d\n", test.seats, result)
}
}
}

Вывод: Проверка после цикла — это не опциональное улучшение, а обязательная часть алгоритма. Без неё решение будет некорректным для всех тестов, где оптимальное место находится в конце массива.

Вопрос 3. Что такое нотация Big O (O-нотация)?

Таймкод: 00:17:01

Ответ собеседника: Правильный. Big O — это функция, оценивающая сложность алгоритмов. Используется для оценки времени выполнения или расхода памяти в зависимости от размера входных данных N. O(N) означает линейную зависимость — примерно N операций. По памяти O(N) означает, что выделяется дополнительная структура данных, зависящая от размера входа.

Правильный ответ:

Определение:

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

Формальное определение:

Функция f(n) = O(g(n)), если существуют константы c > 0 и n₀ ≥ 0, такие что для всех n ≥ n₀ выполняется:

f(n) ≤ c × g(n)

Основные классы сложности:

1. O(1) — Константная

func getFirst(arr []int) int {
return arr[0] // Всегда одна операция
}

2. O(log n) — Логарифмическая

func binarySearch(arr []int, target int) int {
left, right := 0, len(arr)-1
for left <= right {
mid := left + (right-left)/2
if arr[mid] == target {
return mid
} else if arr[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}

3. O(n) — Линейная

func linearSearch(arr []int, target int) int {
for i, v := range arr {
if v == target {
return i
}
}
return -1
}

4. O(n log n) — Линейно-логарифмическая

func mergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
mid := len(arr) / 2
left := mergeSort(arr[:mid])
right := mergeSort(arr[mid:])
return merge(left, right)
}

5. O(n²) — Квадратичная

func bubbleSort(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}

6. O(2ⁿ) — Экспоненциальная

func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}

Принципы упрощения:

1. Отбрасывание констант:

  • O(2n) → O(n)
  • O(100) → O(1)

2. Отбрасывание младших членов:

  • O(n² + n) → O(n²)
  • O(n + log n) → O(n)

3. Примеры упрощения:

// O(3n² + 5n + 100) упрощается до O(n²)
func example(arr []int) {
n := len(arr)
// O(1)
x := 0

// O(n)
for i := 0; i < n; i++ {
x += arr[i]
}

// O(n²)
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
x += arr[i] * arr[j]
}
}

// O(n)
for i := 0; i < n; i++ {
x -= arr[i]
}
}

Пространственная сложность:

// O(1) по памяти
func sum(arr []int) int {
total := 0
for _, v := range arr {
total += v
}
return total
}

// O(n) по памяти
func copyArray(arr []int) []int {
result := make([]int, len(arr))
copy(result, arr)
return result
}

// O(n²) по памяти
func matrix(n int) [][]int {
m := make([][]int, n)
for i := range m {
m[i] = make([]int, n)
}
return m
}

Лучший, средний и худший случаи:

// Лучший случай: O(1) — элемент найден сразу
// Средний случай: O(n) — элемент где-то в середине
// Худший случай: O(n) — элемент в конце или отсутствует
func search(arr []int, target int) int {
for i, v := range arr {
if v == target {
return i
}
}
return -1
}

Практическое сравнение:

N | O(1) | O(log n) | O(n) | O(n log n) | O(n²)
--------|------|----------|------|------------|-------
10 | 1 | 3 | 10 | 33 | 100
100 | 1 | 7 | 100 | 664 | 10,000
1,000 | 1 | 10 | 1,000| 9,966 | 1,000,000
10,000 | 1 | 13 | 10,000| 132,877 | 100,000,000

Другие нотации:

  • Ω (Омега) — нижняя граница (лучший случай)
  • Θ (Тета) — точная оценка (когда верхняя и нижняя границы совпадают)

Вывод: Big O позволяет сравнивать алгоритмы без привязки к конкретному железу и предсказывать их поведение на больших объёмах данных.

Вопрос 4. Что такое хеш-таблица?

Таймкод: 00:18:26

Ответ собеседника: Правильный. Хеш-таблица — это структура данных, содержащая пары ключ-значение. Поиск в среднем случае O(1) — нахождение значения по хешу ключа. При коллизиях (одинаковый хеш для разных ключей) сложность может вырасти до O(N). Вставка в Go в основном O(1), но может потребовать выделения дополнительных бакетов.

Правильный ответ:

Определение:

Хеш-таблица (hash map, ассоциативный массив, словарь) — это структура данных, реализующая интерфейс ассоциативного массива, которая хранит пары ключ-значение и позволяет выполнять операции вставки, поиска и удаления за амортизированное O(1).

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

1. Хеш-функция преобразует ключ в индекс массива:

hash(key) → index

2. Бакеты (buckets) — ячейки внутреннего массива, в которых хранятся пары ключ-значение.

Коллизии и методы их разрешения:

A. Метод цепочек (Separate Chaining)

Каждый бакет содержит связный список элементов с одинаковым хешем.

type Node struct {
key string
value int
next *Node
}

type HashMap struct {
buckets []*Node
size int
}

func (h *HashMap) Put(key string, value int) {
index := h.hash(key) % h.size
node := h.buckets[index]

// Ищем существующий ключ в цепочке
for node != nil {
if node.key == key {
node.value = value // Обновляем
return
}
node = node.next
}

// Добавляем новый узел в начало цепочки
newNode := &Node{key, value, h.buckets[index]}
h.buckets[index] = newNode
}

B. Открытая адресация (Open Addressing)

При коллизии ищется следующая свободная ячейка.

type Entry struct {
key string
value int
deleted bool
}

type HashMap struct {
entries []Entry
size int
count int
}

func (h *HashMap) Put(key string, value int) {
if h.count >= h.size*3/4 {
h.resize()
}

index := h.hash(key) % h.size

for h.entries[index].key != "" && !h.entries[index].deleted {
if h.entries[index].key == key {
h.entries[index].value = value
return
}
index = (index + 1) % h.size // Линейное пробирование
}

h.entries[index] = Entry{key, value, false}
h.count++
}

Реализация хеш-таблицы в Go:

Go использует встроенный тип map, который является хеш-таблицей:

// Создание и использование
m := make(map[string]int)

// Вставка
m["apple"] = 5
m["banana"] = 3

// Поиск
if val, ok := m["apple"]; ok {
fmt.Println("Found:", val)
}

// Удаление
delete(m, "banana")

// Итерация
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}

Внутренняя структура map в Go:

// Упрощённая структура (runtime/map.go)
type hmap struct {
count int // Количество элементов
flags uint8
B uint8 // Логарифм количества бакетов (2^B)
noverflow uint16
hash0 uint32 // Сид для хеш-функции

buckets unsafe.Pointer // Массив бакетов
oldbuckets unsafe.Pointer // Предыдущие бакеты (при росте)
nevacuate uintptr // Прогресс эвакуации
}

// Структура бакета
type bmap struct {
tophash [8]uint8 // Верхние биты хеша для быстрого сравнения
keys [8]byte // Ключи
values [8]byte // Значения
overflow uintptr // Указатель на переполненный бакет
}

Сложность операций:

ОперацияСредний случайХудший случай
ПоискO(1)O(n)
ВставкаO(1)O(n)
УдалениеO(1)O(n)

Худший случай возникает при:

  • Множественных коллизиях
  • Плохой хеш-функции
  • Атаках на хеш-таблицу (HashDoS)

Фактор загрузки (Load Factor):

load_factor = количество_элементов / количество_бакетов

При превышении порога (обычно 0.75) происходит рехеширование — создание большего массива бакетов.

Рехеширование в Go:

// При превышении load factor:
// 1. Создаётся новый массив бакетов в 2 раза больше
// 2. Элементы постепенно переносятся (incremental copying)
// 3. Старый массив освобождается после полного переноса

Пример реализации простой хеш-таблицы:

package main

import "fmt"

const defaultSize = 16

type Entry struct {
key string
value interface{}
next *Entry
}

type HashMap struct {
buckets []*Entry
size int
}

func NewHashMap() *HashMap {
return &HashMap{
buckets: make([]*Entry, defaultSize),
size: defaultSize,
}
}

func (h *HashMap) hash(key string) int {
hash := 0
for _, ch := range key {
hash = 31*hash + int(ch)
}
return hash & (h.size - 1) // Быстрое взятие модуля для степеней двойки
}

func (h *HashMap) Put(key string, value interface{}) {
index := h.hash(key)

// Проверяем существующий ключ
for entry := h.buckets[index]; entry != nil; entry = entry.next {
if entry.key == key {
entry.value = value
return
}
}

// Добавляем новый entry
newEntry := &Entry{key, value, h.buckets[index]}
h.buckets[index] = newEntry
}

func (h *HashMap) Get(key string) (interface{}, bool) {
index := h.hash(key)

for entry := h.buckets[index]; entry != nil; entry = entry.next {
if entry.key == key {
return entry.value, true
}
}
return nil, false
}

func main() {
hm := NewHashMap()
hm.Put("name", "John")
hm.Put("age", 30)

if val, ok := hm.Get("name"); ok {
fmt.Println("name:", val)
}
}

Особенности map в Go:

  1. Порядок итерации не гарантирован — при каждом range порядок может быть разным
  2. Не потокобезопасна — для конкурентного доступа нужна синхронизация или sync.Map
  3. Ключи должны быть сравнимыми — нельзя использовать slice, map, function как ключи
  4. Нулевое значение — nil map, запись в которую вызовет panic

Когда использовать:

  • Быстрый поиск по ключу
  • Подсчёт частот
  • Дедупликация
  • Кэширование
  • Реализация множеств (set)

Вопрос 5. Что такое очередь с приоритетом и на каких структурах данных её можно реализовать?

Таймкод: 00:19:35

Ответ собеседника: Правильный. Обычная очередь работает по принципу FIFO (первый зашёл — первый вышел). Очередь с приоритетом определяет порядок извлечения элементов по приоритету. Реализуется на основе кучи (heap).

Правильный ответ:

Определение:

Очередь с приоритетом (Priority Queue) — это абстрактный тип данных, где каждый элемент имеет приоритет, и элемент с наивысшим приоритетом извлекается первым, независимо от порядка добавления.

Отличие от обычной очереди:

Обычная очередьОчередь с приоритетом
FIFO (First In First Out)По приоритету
Порядок определяется временем добавленияПорядок определяется значением приоритета

Структуры данных для реализации:

1. Куча (Heap) — наиболее эффективная

Бинарная куча — это полное бинарное дерево, где каждый родитель меньше (min-heap) или больше (max-heap) своих детей.

package main

import (
"container/heap"
"fmt"
)

// Элемент очереди
type Item struct {
value string
priority int
index int // Индекс в куче
}

// Min-heap реализация
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].priority < pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil
item.index = -1
*pq = old[0 : n-1]
return item
}

func main() {
pq := make(PriorityQueue, 0)
heap.Init(&pq)

heap.Push(&pq, &Item{value: "task1", priority: 3})
heap.Push(&pq, &Item{value: "task2", priority: 1})
heap.Push(&pq, &Item{value: "task3", priority: 2})

for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("Processing: %s (priority: %d)\n", item.value, item.priority)
}
}

Сложность операций на куче:

ОперацияСложность
InsertO(log n)
ExtractO(log n)
PeekO(1)

2. Отсортированный массив/список

type SortedPriorityQueue struct {
items []Item
}

func (pq *SortedPriorityQueue) Insert(item Item) {
// Находим позицию для вставки
i := 0
for i < len(pq.items) && pq.items[i].priority <= item.priority {
i++
}
// Вставляем с сохранением порядка
pq.items = append(pq.items[:i], append([]Item{item}, pq.items[i:]...)...)
}

func (pq *SortedPriorityQueue) Extract() Item {
item := pq.items[0]
pq.items = pq.items[1:]
return item
}

Сложность:

ОперацияСложность
InsertO(n)
ExtractO(1)

3. Связный список

type ListNode struct {
item Item
next *ListNode
}

type LinkedListPQ struct {
head *ListNode
}

func (pq *LinkedListPQ) Insert(item Item) {
newNode := &ListNode{item: item}

// Вставка в начало или поиск позиции
if pq.head == nil || item.priority < pq.head.item.priority {
newNode.next = pq.head
pq.head = newNode
} else {
current := pq.head
for current.next != nil && current.next.item.priority <= item.priority {
current = current.next
}
newNode.next = current.next
current.next = newNode
}
}

4. Двоичное дерево поиска (BST)

type TreeNode struct {
item Item
left *TreeNode
right *TreeNode
}

type BSTPriorityQueue struct {
root *TreeNode
}

func (pq *BSTPriorityQueue) Insert(item Item) {
pq.root = insertNode(pq.root, item)
}

func insertNode(node *TreeNode, item Item) *TreeNode {
if node == nil {
return &TreeNode{item: item}
}
if item.priority < node.item.priority {
node.left = insertNode(node.left, item)
} else {
node.right = insertNode(node.right, item)
}
return node
}

Сравнение реализаций:

СтруктураInsertExtractPeekПамять
КучаO(log n)O(log n)O(1)O(n)
Отсортированный массивO(n)O(1)O(1)O(n)
Связный списокO(n)O(1)O(1)O(n)
BST (сбалансированное)O(log n)O(log n)O(log n)O(n)

Практические применения:

1. Алгоритм Дейкстры (кратчайший путь):

func dijkstra(graph map[string]map[string]int, start string) map[string]int {
dist := make(map[string]int)
for node := range graph {
dist[node] = math.MaxInt32
}
dist[start] = 0

pq := make(PriorityQueue, 0)
heap.Init(&pq)
heap.Push(&pq, &Item{value: start, priority: 0})

for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
u := item.value

for v, weight := range graph[u] {
alt := dist[u] + weight
if alt < dist[v] {
dist[v] = alt
heap.Push(&pq, &Item{value: v, priority: alt})
}
}
}
return dist
}

2. Планировщик задач:

type Task struct {
name string
priority int
deadline time.Time
}

type TaskScheduler struct {
queue PriorityQueue
}

func (s *TaskScheduler) AddTask(task Task) {
heap.Push(&s.queue, &Item{
value: task.name,
priority: task.priority,
})
}

func (s *TaskScheduler) NextTask() string {
if s.queue.Len() == 0 {
return ""
}
item := heap.Pop(&s.queue).(*Item)
return item.value
}

3. Слияние K отсортированных списков:

func mergeKLists(lists [][]int) []int {
pq := make(PriorityQueue, 0)
heap.Init(&pq)

// Добавляем первые элементы
for i, list := range lists {
if len(list) > 0 {
heap.Push(&pq, &Item{
value: list[0],
priority: list[0],
index: i,
})
}
}

result := []int{}
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
result = append(result, item.value)

// Добавляем следующий элемент из того же списка
listIdx := item.index
if len(lists[listIdx]) > 1 {
lists[listIdx] = lists[listIdx][1:]
heap.Push(&pq, &Item{
value: lists[listIdx][0],
priority: lists[listIdx][0],
index: listIdx,
})
}
}
return result
}

Вывод: Куча — оптимальная структура для очереди с приоритетом, обеспечивающая O(log n) для вставки и извлечения. В Go используется пакет container/heap с интерфейсом, который нужно реализовать.

Вопрос 6. Что такое указатель и сколько он весит?

Таймкод: 00:21:11

Ответ собеседника: Правильный. Указатель — это тип данных, содержащий адрес ячейки памяти, на которую он ссылается. Размер указателя зависит от архитектуры: 4 байта на 32-битной системе, 8 байт на 64-битной.

Правильный ответ:

Определение:

Указатель — это переменная, хранящая адрес в памяти, по которому расположены данные другого типа. Указатель "указывает" на расположение значения, а не содержит само значение.

Размер указателя:

АрхитектураРазмер указателя
32-bit4 байта
64-bit8 байт
package main

import (
"fmt"
"unsafe"
)

func main() {
var p *int
var s *string
var m *map[string]int

fmt.Println("Размер *int:", unsafe.Sizeof(p)) // 8 на 64-bit
fmt.Println("Размер *string:", unsafe.Sizeof(s)) // 8 на 64-bit
fmt.Println("Размер *map:", unsafe.Sizeof(m)) // 8 на 64-bit
}

Важно: Размер указателя не зависит от типа данных, на которые он указывает. Все указатели на одной архитектуре имеют одинаковый размер.

Основные операции с указателями:

1. Объявление и инициализация:

var p *int // nil указатель
x := 42
p = &x // Получаем адрес переменной x

2. Разыменование (dereferencing):

x := 42
p := &x

fmt.Println(*p) // 42 - получаем значение по адресу
*p = 100 // Изменяем значение по адресу
fmt.Println(x) // 100

3. Нулевой указатель:

var p *int
if p == nil {
fmt.Println("Указатель nil")
}

// Разыменование nil указателя вызывает panic
// *p = 42 // panic: runtime error

Указатели в структурах:

type Person struct {
Name string
Age int
}

func main() {
p := &Person{Name: "John", Age: 30}

// Автоматическое разыменование
fmt.Println(p.Name) // Эквивалентно (*p).Name
p.Age = 31 // Эквивалентно (*p).Age = 31
}

Указатели на указатели:

x := 42
p := &x
pp := &p

fmt.Println(**pp) // 42
**pp = 100
fmt.Println(x) // 100

Указатели и функции:

// Передача по значению (копия)
func incrementValue(x int) {
x++
}

// Передача по указателю (оригинал)
func incrementPointer(x *int) {
*x++
}

func main() {
a := 10
incrementValue(a)
fmt.Println(a) // 10 - не изменилось

incrementPointer(&a)
fmt.Println(a) // 11 - изменилось
}

Указатели и срезы:

// Срез уже содержит указатель на внутренний массив
func modifySlice(s []int) {
s[0] = 100 // Изменит оригинал
s = append(s, 99) // Не изменит оригинал (новая копия заголовка)
}

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

Unsafe указатели:

package main

import (
"fmt"
"unsafe"
)

func main() {
x := 42
p := unsafe.Pointer(&x)

// Приведение типов указателей
intPtr := (*int)(p)
fmt.Println(*intPtr) // 42

// Арифметика указателей
arr := [3]int{10, 20, 30}
base := unsafe.Pointer(&arr[0])

// Получаем адрес второго элемента
second := unsafe.Pointer(uintptr(base) + unsafe.Sizeof(arr[0]))
fmt.Println(*(*int)(second)) // 20
}

Размер различных типов данных в Go:

package main

import (
"fmt"
"unsafe"
)

func main() {
fmt.Println("=== Размеры типов ===")
fmt.Println("int:", unsafe.Sizeof(int(0)))
fmt.Println("int8:", unsafe.Sizeof(int8(0)))
fmt.Println("int16:", unsafe.Sizeof(int16(0)))
fmt.Println("int32:", unsafe.Sizeof(int32(0)))
fmt.Println("int64:", unsafe.Sizeof(int64(0)))
fmt.Println("uint:", unsafe.Sizeof(uint(0)))
fmt.Println("float32:", unsafe.Sizeof(float32(0)))
fmt.Println("float64:", unsafe.Sizeof(float64(0)))
fmt.Println("bool:", unsafe.Sizeof(false))
fmt.Println("byte:", unsafe.Sizeof(byte(0)))
fmt.Println("rune:", unsafe.Sizeof(rune(0)))

fmt.Println("\n=== Размеры составных типов ===")
fmt.Println("string:", unsafe.Sizeof(""))
fmt.Println("[]int:", unsafe.Sizeof([]int{}))
fmt.Println("map[string]int:", unsafe.Sizeof(map[string]int{}))
fmt.Println("chan int:", unsafe.Sizeof(make(chan int)))
fmt.Println("func():", unsafe.Sizeof(func() {}))

fmt.Println("\n=== Размеры указателей ===")
fmt.Println("*int:", unsafe.Sizeof((*int)(nil)))
fmt.Println("*string:", unsafe.Sizeof((*string)(nil)))
fmt.Println("*struct{}:", unsafe.Sizeof((*struct{})(nil)))
}

Выравнивание и padding:

type Example struct {
a bool // 1 байт + 7 байт padding
b int64 // 8 байт
c bool // 1 байт + 7 байт padding
}

// Размер структуры: 24 байта (с padding)
// Без padding было бы: 10 байт

func main() {
fmt.Println("Size of Example:", unsafe.Sizeof(Example{})) // 24
fmt.Println("Offset of a:", unsafe.Offsetof(Example{}.a)) // 0
fmt.Println("Offset of b:", unsafe.Offsetof(Example{}.b)) // 8
fmt.Println("Offset of c:", unsafe.Offsetof(Example{}.c)) // 16
}

Когда использовать указатели:

  1. Изменение аргументов функции
  2. Избежание копирования больших структур
  3. Реализация связных структур данных (списки, деревья)
  4. Полиморфизм через интерфейсы
  5. Опциональные поля (nil означает отсутствие значения)

Проблемы с указателями:

// 1. Nil pointer dereference
var p *int
// *p = 42 // panic

// 2. Висячий указатель (dangling pointer)
func newInt() *int {
x := 42
return &x // Go переместит x в кучу (escape analysis)
}

// 3. Утечка памяти через циклические ссылки
type Node struct {
next *Node
}

Вывод: Указатель — это фундаментальная концепция для работы с памятью. На 64-битных системах указатель всегда занимает 8 байт независимо от типа данных, на которые он ссылается.

Вопрос 7. В чём разница между процессом и потоком?

Таймкод: 00:21:52

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

Правильный ответ:

Определения:

Процесс — это экземпляр выполняемой программы, который имеет собственное изолированное адресное пространство памяти, ресурсы (файловые дескрипторы, сокеты) и как минимум один поток выполнения.

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

Ключевые различия:

ХарактеристикаПроцессПоток
ПамятьИзолированное адресное пространствоРазделяет память с другими потоками процесса
СозданиеДорогое (копирование контекста)Дешёвое (уже есть адресное пространство)
Переключение контекстаДорогоеДешевле
Обмен даннымиIPC (pipes, sockets, shared memory)Прямой доступ к разделяемой памяти
ИзоляцияПолная (сбой не влияет на другие)Нет (сбой потока = сбой процесса)
Накладные расходыБольшиеМаленькие

Структура процесса в памяти:

┌─────────────────────────┐
│ Stack │ ← Локальные переменные, вызовы функций
├─────────────────────────┤
│ ↓ │
│ Heap │ ← Динамически выделенная память
│ ↑ │
├─────────────────────────┤
│ Data │ ← Глобальные и статические переменные
├─────────────────────────┤
│ Code │ ← Машинный код программы
└─────────────────────────┘

Структура потоков:

Процесс:
┌─────────────────────────────────────────┐
│ Code │ Data │ Heap │ Files │ Signals │ ← Разделяется всеми потоками
├─────────────────────────────────────────┤
│ Поток 1: │ Stack │ Registers │ PC │ ← Уникально для каждого потока
├─────────────────────────────────────────┤
│ Поток 2: │ Stack │ Registers │ PC │
├─────────────────────────────────────────┤
│ Поток 3: │ Stack │ Registers │ PC │
└─────────────────────────────────────────┘

Пример на Go:

package main

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

// Разделяемые данные (в адресном пространстве процесса)
var counter int
var mu sync.Mutex

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()

for i := 0; i < 1000; i++ {
mu.Lock()
counter++ // Потоки разделяют эту переменную
mu.Unlock()
}

fmt.Printf("Worker %d finished\n", id)
}

func main() {
var wg sync.WaitGroup

// Запускаем несколько горутин (потоков)
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(i, &wg)
}

wg.Wait()
fmt.Println("Counter:", counter)
}

Горутины vs ОС-потоки:

Go использует M:N модель — множество горутин маппится на меньшее количество ОС-потоков:

package main

import (
"fmt"
"runtime"
)

func main() {
// Количество логических процессоров
fmt.Println("NumCPU:", runtime.NumCPU())

// Устанавливаем количество ОС-потоков
runtime.GOMAXPROCS(4)

// Горутины легче ОС-потоков:
// - Стек горутины начинается с ~2KB (растёт динамически)
// - Стек ОС-потока обычно 1-8MB (фиксированный)
}

Сравнение характеристик:

package main

import (
"fmt"
"os"
"os/exec"
"runtime"
"time"
)

// Пример работы с процессами
func processExample() {
cmd := exec.Command("ls", "-la")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
fmt.Println("Error:", err)
}
}

// Пример работы с горутинами
func goroutineExample() {
ch := make(chan string)

go func() {
time.Sleep(100 * time.Millisecond)
ch <- "Hello from goroutine"
}()

msg := <-ch
fmt.Println(msg)
}

func main() {
fmt.Println("OS:", runtime.GOOS)
fmt.Println("Arch:", runtime.GOARCH)
fmt.Println("NumCPU:", runtime.NumCPU())

processExample()
goroutineExample()
}

Межпроцессное взаимодействие (IPC):

// 1. Через каналы (pipes)
func pipeExample() {
cmd := exec.Command("echo", "hello")
output, _ := cmd.Output()
fmt.Println(string(output))
}

// 2. Через файлы
func fileIPC() {
os.WriteFile("data.txt", []byte("hello"), 0644)
data, _ := os.ReadFile("data.txt")
fmt.Println(string(data))
}

// 3. Через сокеты
// 4. Через shared memory (syscall.Mmap)

Проблемы многопоточности:

// Race condition
func raceCondition() {
var counter int
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // DATA RACE!
}()
}
wg.Wait()
}

// Deadlock
func deadlock() {
mu1 := sync.Mutex{}
mu2 := sync.Mutex{}

go func() {
mu1.Lock()
time.Sleep(10 * time.Millisecond)
mu2.Lock() // Ждёт mu2
mu2.Unlock()
mu1.Unlock()
}()

mu2.Lock()
time.Sleep(10 * time.Millisecond)
mu1.Lock() // Ждёт mu1 → DEADLOCK
mu1.Unlock()
mu2.Unlock()
}

Когда что использовать:

Процессы:

  • Изоляция задач (микросервисы)
  • Разные приложения
  • Безопасность (песочница)
  • Надёжность (сбой не влияет на другие)

Потоки/горутины:

  • Параллельная обработка данных
  • I/O-bound задачи
  • Конкурентные запросы
  • Разделяемые данные

Вывод: Процессы обеспечивают изоляцию и безопасность, но требуют больше ресурсов. Потоки легче и быстрее создаются, но требуют синхронизации при доступе к разделяемым данным. В Go горутины — это легковесная абстракция поверх ОС-потоков.

Вопрос 8. Что делает команда Fork в контексте процессов?

Таймкод: 00:22:52

Ответ собеседника: Правильный. Fork создаёт дочерний процесс, который является копией текущего (родительского) процесса. Дочерний процесс запускается на основе родительского, копируя его адресное пространство.

Правильный ответ:

Определение:

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

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

До fork():
┌─────────────────┐
│ Родительский │
│ процесс │
└─────────────────┘

После fork():
┌─────────────────┐ ┌─────────────────┐
│ Родительский │ │ Дочерний │
│ процесс │ │ процесс │
│ pid = 1234 │ │ pid = 1235 │
│ ppid = 1000 │ │ ppid = 1234 │
└─────────────────┘ └─────────────────┘

Возвращаемые значения:

pid_t pid = fork();

if (pid < 0) {
// Ошибка создания процесса
perror("fork failed");
} else if (pid == 0) {
// Код выполняется в дочернем процессе
printf("Я дочерний процесс, PID = %d\n", getpid());
} else {
// Код выполняется в родительском процессе
printf("Я родительский процесс, PID потомка = %d\n", pid);
}

Что копируется при fork:

  • Адресное пространство процесса (код, данные, стек, куча)
  • Файловые дескрипторы (с общими смещениями)
  • Обработчики сигналов
  • Текущий рабочий каталог
  • Маску режима создания файлов (umask)

Что НЕ копируется:

  • PID (идентификатор процесса)
  • PPID (идентификатор родительского процесса)
  • Блокировки памяти
  • Ожидающие сигналы
  • Таймеры

Пример на Go (через syscall):

package main

import (
"fmt"
"os"
"syscall"
)

func main() {
fmt.Printf("До fork: PID = %d\n", os.Getpid())

pid, _, errno := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)

if errno != 0 {
fmt.Println("Ошибка fork:", errno)
return
}

if pid == 0 {
// Дочерний процесс
fmt.Printf("Дочерний: PID = %d, PPID = %d\n",
os.Getpid(), os.Getppid())
} else {
// Родительский процесс
fmt.Printf("Родительский: PID = %d, PID потомка = %d\n",
os.Getpid(), pid)

// Ждём завершения дочернего процесса
var status syscall.WaitStatus
syscall.Wait4(int(pid), &status, 0, nil)
}
}

Copy-on-Write (COW):

Современные ОС используют оптимизацию COW — физическая память копируется только при попытке записи:

fork():
┌──────────────┐ ┌──────────────┐
│ Родитель │ │ Дочерний │
│ Страница │────▶│ Страница │
│ (read) │ │ (read) │
└──────────────┘ └──────────────┘
│ │
└──── Общая ─────────┘
физическая
память

После записи одним из процессов:
┌──────────────┐ ┌──────────────┐
│ Родитель │ │ Дочерний │
│ Страница │ │ Страница │
│ (copy) │ │ (copy) │
└──────────────┘ └──────────────┘
│ │
▼ ▼
Отдельная Отдельная
физ. память физ. память

Типичный паттерн fork + exec:

package main

import (
"fmt"
"os"
"os/exec"
)

func main() {
// Более идиоматичный способ в Go
cmd := exec.Command("ls", "-la", "/tmp")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
fmt.Println("Ошибка:", err)
}
}

Разница fork и exec:

fork()exec()
Создаёт копию текущего процессаЗаменяет текущий процесс новой программой
Продолжает выполнение той же программыНачинает выполнение новой программы
Возвращает PID потомкаНе возвращает управление при успехе

Применение fork:

1. Серверы (prefork model):

// Упрощённая модель
func preforkServer() {
workers := make([]int, 4)

for i := 0; i < 4; i++ {
pid, _, _ := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
if pid == 0 {
// Дочерний процесс - обрабатывает запросы
handleConnections()
os.Exit(0)
} else {
workers[i] = int(pid)
}
}

// Родитель ждёт завершения детей
for _, pid := range workers {
syscall.Wait4(pid, nil, 0, nil)
}
}

2. Параллельная обработка:

func parallelProcessing(data []int) {
mid := len(data) / 2

pid, _, _ := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)

if pid == 0 {
// Обрабатываем вторую половину
process(data[mid:])
os.Exit(0)
} else {
// Обрабатываем первую половину
process(data[:mid])
syscall.Wait4(int(pid), nil, 0, nil)
}
}

Проблемы fork:

// 1. Проблема с файловыми дескрипторами
func fdIssue() {
file, _ := os.Open("data.txt")
defer file.Close()

// После fork оба процесса имеют один файловый дескриптор
// Закрытие в одном процессе не влияет на другой
}

// 2. Проблема с блокировками
func lockIssue() {
mu.Lock()

// Если fork происходит с заблокированным мьютексом,
// дочерний процесс может зависнуть при попытке блокировки

pid, _, _ := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
mu.Unlock()
}

Вывод: fork() — фундаментальный механизм Unix для создания процессов. Он создаёт точную копию родительского процесса с оптимизацией COW. Обычно используется в связке с exec() для запуска новых программ или для создания многопроцессных серверов.

Вопрос 9. Что такое load average (средняя загрузка)?

Таймкод: 00:23:49

Ответ собеседника: Правильный. Load average — это среднее количество процессов, выполняющихся на процессоре (CPU). Можно задавать интервалы для оценки загрузки, например за последнюю минуту, 5 минут, 15 минут.

Правильный ответ:

Определение:

Load average — это метрика, показывающая среднее количество процессов, которые находятся в состоянии выполнения (running) или ожидания ввода/вывода (uninterruptible sleep) за определённые периоды времени (обычно 1, 5 и 15 минут).

Что показывает load average:

$ uptime
14:30:00 up 10 days, 3:45, 2 users, load average: 1.50, 0.80, 0.60

Три числа означают:

  • 1.50 — средняя загрузка за последнюю 1 минуту
  • 0.80 — средняя загрузка за последние 5 минут
  • 0.60 — средняя загрузка за последние 15 минут

Состояния процессов, учитываемые в load average:

R (running) - Процесс выполняется на CPU
D (disk sleep) - Процесс ожидает I/O (uninterruptible sleep)

Интерпретация значений:

Load AverageИнтерпретация
0.0 - 0.7Низкая загрузка
0.7 - 1.0Умеренная загрузка
1.0 - NВысокая загрузка (N = количество CPU)
> NПерегрузка

Пример для системы с 4 CPU:

Load average: 4.00 → 100% загрузка (все CPU заняты)
Load average: 8.00 → перегрузка в 2 раза (очередь процессов)
Load average: 2.00 → 50% загрузка

Как рассчитать оптимальную нагрузку:

package main

import (
"fmt"
"runtime"
)

func main() {
numCPU := runtime.NumCPU()
fmt.Printf("Количество CPU: %d\n", numCPU)

// Оптимальная нагрузка примерно равна количеству CPU
fmt.Printf("Оптимальный load average: %.1f\n", float64(numCPU)*0.7)
fmt.Printf("Максимальный load average: %d\n", numCPU)
}

Получение load average в Go:

package main

import (
"fmt"
"github.com/shirou/gopsutil/v3/load"
)

func main() {
avg, err := load.Avg()
if err != nil {
panic(err)
}

fmt.Printf("Load average (1 min): %.2f\n", avg.Load1)
fmt.Printf("Load average (5 min): %.2f\n", avg.Load5)
fmt.Printf("Load average (15 min): %.2f\n", avg.Load15)
}

Load average vs CPU utilization:

МетрикаЧто показывает
Load averageКоличество процессов в очереди
CPU utilizationПроцент времени, когда CPU занят

Примеры ситуаций:

1. CPU-bound нагрузка:

// Вычислительно интенсивные задачи
func cpuIntensive() {
for {
// Тяжёлые вычисления
_ = fibonacci(40)
}
}
// Load average будет расти пропорционально количеству горутин

2. I/O-bound нагрузка:

// Множество запросов к диску или сети
func ioIntensive() {
for i := 0; i < 1000; i++ {
go func() {
resp, _ := http.Get("http://api.example.com")
defer resp.Body.Close()
}()
}
}
// Load average может быть высокой при низкой загрузке CPU

Мониторинг в production:

package main

import (
"context"
"fmt"
"time"

"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/cpu"
)

func monitorSystem(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
avg, _ := load.Avg()
cpuPercent, _ := cpu.Percent(0, false)

fmt.Printf("[%s] Load: %.2f %.2f %.2f | CPU: %.1f%%\n",
time.Now().Format("15:04:05"),
avg.Load1, avg.Load5, avg.Load15,
cpuPercent[0])
}
}
}

Проблемы при высоком load average:

Symptoms:
- Медленный отклик системы
- Таймауты запросов
- Увеличение latency
- OOM killer активируется

Causes:
- Утечка горутин/процессов
- Медленные запросы к БД
- Блокировки в коде
- Недостаточно ресурсов

Диагностика:

# Просмотр load average
uptime
cat /proc/loadavg

# Просмотр процессов в состоянии D (uninterruptible sleep)
ps aux | awk '$8 ~ /D/ {print}'

# Мониторинг в реальном времени
htop
atop

Вывод: Load average — важная метрика для понимания загрузки системы. Значение выше количества CPU ядер указывает на перегрузку. Важно различать CPU-bound и I/O-bound нагрузку для правильной оптимизации.

Вопрос 10. Что такое deadlock (взаимная блокировка)?

Таймкод: 00:24:27

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

Правильный ответ:

Определение:

Deadlock (взаимная блокировка) — это ситуация в многопоточной среде, при которой два или более потоков находятся в состоянии бесконечного ожидания, потому что каждый из них удерживает ресурс и ждёт освобождения ресурса, занятого другим потоком.

Условия возникновения deadlock (условия Коффмана):

Все четыре условия должны выполняться одновременно:

  1. Взаимное исключение (Mutual Exclusion) — ресурс может использоваться только одним потоком
  2. Удержание и ожидание (Hold and Wait) — поток удерживает ресурс и ждёт другой
  3. Невозможность вытеснения (No Preemption) — ресурс не может быть принудительно отобран
  4. Циклическое ожидание (Circular Wait) — существует цикл ожидания между потоками

Классический пример deadlock:

package main

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

func main() {
mu1 := sync.Mutex{}
mu2 := sync.Mutex{}

// Горутина 1: сначала блокирует mu1, потом mu2
go func() {
mu1.Lock()
fmt.Println("Горутина 1: заблокировала mu1")
time.Sleep(100 * time.Millisecond) // Даём время второй горутине

mu2.Lock() // DEADLOCK: mu2 уже заблокирована горутиной 2
fmt.Println("Горутина 1: заблокировала mu2")
mu2.Unlock()
mu1.Unlock()
}()

// Горутина 2: сначала блокирует mu2, потом mu1
go func() {
mu2.Lock()
fmt.Println("Горутина 2: заблокировала mu2")
time.Sleep(100 * time.Millisecond)

mu1.Lock() // DEADLOCK: mu1 уже заблокирована горутиной 1
fmt.Println("Горутина 2: заблокировала mu1")
mu1.Unlock()
mu2.Unlock()
}()

time.Sleep(2 * time.Second) // Программа зависнет
}

Визуализация deadlock:

Горутина 1 Горутина 2
│ │
├── Lock(mu1) ✓ │
│ ├── Lock(mu2) ✓
│ │
├── Lock(mu2) ⏳ │
│ (ждёт) ├── Lock(mu1) ⏳
│ │ (ждёт)
▼ ▼
DEADLOCK! DEADLOCK!

Решения проблемы deadlock:

1. Упорядочивание блокировок (Lock Ordering):

package main

import (
"fmt"
"sync"
)

type Account struct {
ID int
Balance int
mu sync.Mutex
}

func transfer(from, to *Account, amount int) {
// Всегда блокируем в порядке ID
first, second := from, to
if from.ID > to.ID {
first, second = to, from
}

first.mu.Lock()
defer first.mu.Unlock()

second.mu.Lock()
defer second.mu.Unlock()

from.Balance -= amount
to.Balance += amount
}

func main() {
acc1 := &Account{ID: 1, Balance: 1000}
acc2 := &Account{ID: 2, Balance: 1000}

var wg sync.WaitGroup

// Безопасные переводы в обоих направлениях
for i := 0; i < 100; i++ {
wg.Add(2)
go func() {
defer wg.Done()
transfer(acc1, acc2, 10)
}()
go func() {
defer wg.Done()
transfer(acc2, acc1, 10)
}()
}

wg.Wait()
fmt.Printf("Account 1: %d\n", acc1.Balance)
fmt.Printf("Account 2: %d\n", acc2.Balance)
}

2. Использование TryLock с таймаутом:

package main

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

type TryMutex struct {
ch chan struct{}
}

func NewTryMutex() *TryMutex {
return &TryMutex{ch: make(chan struct{}, 1)}
}

func (m *TryMutex) Lock() {
m.ch <- struct{}{}
}

func (m *TryMutex) Unlock() {
<-m.ch
}

func (m *TryMutex) TryLock(timeout time.Duration) bool {
select {
case m.ch <- struct{}{}:
return true
case <-time.After(timeout):
return false
}
}

func main() {
mu1 := NewTryMutex()
mu2 := NewTryMutex{}

go func() {
mu1.Lock()
time.Sleep(100 * time.Millisecond)

if mu2.TryLock(50 * time.Millisecond) {
fmt.Println("Горутина 1: получила оба мьютекса")
mu2.Unlock()
} else {
fmt.Println("Горутина 1: не удалось получить mu2, откат")
}
mu1.Unlock()
}()

go func() {
mu2.Lock()
time.Sleep(100 * time.Millisecond)

if mu1.TryLock(50 * time.Millisecond) {
fmt.Println("Горутина 2: получила оба мьютекса")
mu1.Unlock()
} else {
fmt.Println("Горутина 2: не удалось получить mu1, откат")
}
mu2.Unlock()
}()

time.Sleep(1 * time.Second)
}

3. Использование каналов вместо мьютексов:

package main

import "fmt"

type Resource struct {
data chan int
}

func NewResource() *Resource {
return &Resource{data: make(chan int, 1)}
}

func (r *Resource) Acquire() int {
return <-r.data
}

func (r *Resource) Release(val int) {
r.data <- val
}

func main() {
res1 := NewResource()
res2 := NewResource()

res1.data <- 100
res2.data <- 200

// Каналы предотвращают deadlock при правильном использовании
go func() {
val1 := res1.Acquire()
val2 := res2.Acquire()
fmt.Printf("Получены ресурсы: %d, %d\n", val1, val2)
res1.Release(val1)
res2.Release(val2)
}()
}

4. Использование context с таймаутом:

package main

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

func workerWithTimeout(ctx context.Context, mu1, mu2 *sync.Mutex) error {
// Попытка захватить первый мьютекс с таймаутом
done := make(chan struct{})
go func() {
mu1.Lock()
close(done)
}()

select {
case <-done:
defer mu1.Unlock()
case <-ctx.Done():
return fmt.Errorf("timeout waiting for mu1")
}

// Попытка захватить второй мьютекс
done = make(chan struct{})
go func() {
mu2.Lock()
close(done)
}()

select {
case <-done:
defer mu2.Unlock()
case <-ctx.Done():
return fmt.Errorf("timeout waiting for mu2")
}

return nil
}

func main() {
mu1 := &sync.Mutex{}
mu2 := &sync.Mutex{}

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

err := workerWithTimeout(ctx, mu1, mu2)
if err != nil {
fmt.Println("Ошибка:", err)
}
}

Обнаружение deadlock в Go:

Go runtime обнаруживает deadlock, когда все горутины заблокированы:

package main

import "sync"

func main() {
mu := sync.Mutex{}

mu.Lock()
mu.Lock() // Deadlock: одна горутина дважды блокирует мьютекс

// fatal error: all goroutines are asleep - deadlock!
}

Диагностика deadlock:

// Включение детальной информации о deadlock
// GODEBUG=schedtrace=1000,scheddetail=1

// Использование pprof для анализа
import _ "net/http/pprof"

// Отладка с помощью SIGQUIT
// kill -QUIT <pid>

Паттерны предотвращения deadlock:

1. Иерархия ресурсов:

const (
DB_LOCK = iota
CACHE_LOCK
FILE_LOCK
)

func acquireLocks(locks ...*sync.Mutex) {
// Всегда захватываем в определённом порядке
for _, lock := range locks {
lock.Lock()
}
}

2. Таймауты на все операции:

func safeOperation(mu *sync.Mutex, timeout time.Duration) bool {
ch := make(chan struct{})
go func() {
mu.Lock()
close(ch)
}()

select {
case <-ch:
return true
case <-time.After(timeout):
return false
}
}

3. Lock-free структуры данных:

import "sync/atomic"

type LockFreeCounter struct {
value int64
}

func (c *LockFreeCounter) Increment() {
atomic.AddInt64(&c.value, 1)
}

func (c *LockFreeCounter) Get() int64 {
return atomic.LoadInt64(&c.value)
}

Вывод: Deadlock возникает при одновременном выполнении четырёх условий Коффмана. Основные способы предотвращения: упорядочивание блокировок, таймауты, использование каналов и lock-free структур данных. В Go runtime обнаруживает deadlock, когда все горутины заблокированы.

Вопрос 11. Что такое race condition (состояние гонки)?

Таймкод: 00:25:08

Ответ собеседника: Правильный. Race condition — это ситуация, когда два или более потока имеют общий доступ к ресурсу процесса и одновременно обращаются к нему. При одновременном изменении ресурса может возникнуть некорректное, неожидаемое состояние этого ресурса.

Правильный ответ:

Определение:

Race condition (состояние гонки) — это ситуация, при которой поведение программы зависит от относительного времени выполнения нескольких потоков или горутин, обращающихся к разделяемым данным. Результат выполнения становится недетерминированным и зависит от порядка планирования потоков.

Пример race condition:

package main

import (
"fmt"
"sync"
)

func main() {
var counter int
var wg sync.WaitGroup

// Запускаем 1000 горутин, каждая инкрементирует counter
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // RACE CONDITION!
}()
}

wg.Wait()
fmt.Println("Counter:", counter)
// Результат будет разным при каждом запуске
// Ожидаем: 1000, фактически: 987, 992, 1000, etc.
}

Почему возникает race condition:

Операция counter++ не атомарна. Она состоит из трёх шагов:

1. READ: temp = counter // Читаем текущее значение
2. MODIFY: temp = temp + 1 // Увеличиваем
3. WRITE: counter = temp // Записываем обратно

Временная диаграмма race condition:

Горутина 1 Горутина 2 counter
│ │ 10
├── READ (10) │ 10
│ ├── READ (10) 10
├── MODIFY (11) │ 10
│ ├── MODIFY (11) 10
├── WRITE (11) │ 11
│ ├── WRITE (11) 11 ← Потеряно одно увеличение!

Обнаружение race condition в Go:

Go имеет встроенный race detector:

go run -race main.go
go test -race ./...
go build -race -o app
package main

import "fmt"

func main() {
var counter int

go func() {
for i := 0; i < 1000; i++ {
counter++
}
}()

go func() {
for i := 0; i < 1000; i++ {
counter++
}
}()

fmt.Println("Counter:", counter)
}

Вывод race detector:

==================
WARNING: DATA RACE
Read at 0x00c0000b4010 by goroutine 7:
main.main.func2()
/tmp/main.go:16 +0x4e

Previous write at 0x00c0000b4010 by goroutine 6:
main.main.func1()
/tmp/main.go:10 +0x68
==================

Решения race condition:

1. Мьютексы (sync.Mutex):

package main

import (
"fmt"
"sync"
)

func main() {
var counter int
var mu sync.Mutex
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}

wg.Wait()
fmt.Println("Counter:", counter) // Всегда 1000
}

2. Атомарные операции (sync/atomic):

package main

import (
"fmt"
"sync"
"sync/atomic"
)

func main() {
var counter int64
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1) // Атомарная операция
}()
}

wg.Wait()
fmt.Println("Counter:", atomic.LoadInt64(&counter)) // Всегда 1000
}

3. Каналы (рекомендуемый подход в Go):

package main

import "fmt"

func main() {
counterChan := make(chan int, 1)
counterChan <- 0 // Начальное значение

done := make(chan struct{})

// Одна горутина владеет данными
go func() {
for i := 0; i < 1000; i++ {
counter := <-counterChan
counter++
counterChan <- counter
}
close(done)
}()

go func() {
for i := 0; i < 1000; i++ {
counter := <-counterChan
counter++
counterChan <- counter
}
}()

<-done
fmt.Println("Counter:", <-counterChan) // Всегда 2000
}

4. sync.Map для конкурентного доступа:

package main

import (
"fmt"
"sync"
)

func main() {
var m sync.Map
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(n, n*2)
}(i)
}

wg.Wait()

m.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true
})
}

Сравнение подходов:

ПодходПлюсыМинусы
MutexПростота, гибкостьМожет быть медленным, риск deadlock
AtomicБыстрый для простых операцийОграничен простыми типами
КаналыИдиоматично для Go, безопасноМожет быть многословным
sync.MapОптимизирован для частых чтенийОграниченный API

Примеры race condition в реальном коде:

1. Ленивая инициализация:

// НЕПРАВИЛЬНО
type Singleton struct{}

var instance *Singleton

func GetInstance() *Singleton {
if instance == nil { // Race condition!
instance = &Singleton{}
}
return instance
}

// ПРАВИЛЬНО
var (
instance *Singleton
once sync.Once
)

func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}

2. Кэширование:

// НЕПРАВИЛЬНО
var cache = make(map[string]string)

func GetFromCache(key string) (string, bool) {
val, ok := cache[key] // Race condition при одновременной записи
return val, ok
}

func SetToCache(key, value string) {
cache[key] = value // Race condition
}

// ПРАВИЛЬНО
var (
cache = make(map[string]string)
mu sync.RWMutex
)

func GetFromCache(key string) (string, bool) {
mu.RLock()
defer mu.RUnlock()
val, ok := cache[key]
return val, ok
}

func SetToCache(key, value string) {
mu.Lock()
defer mu.Unlock()
cache[key] = value
}

Типичные ошибки:

// 1. Забыли разблокировать мьютекс
func bad1(mu *sync.Mutex) {
mu.Lock()
if someCondition {
return // Забыли mu.Unlock()!
}
mu.Unlock()
}

// 2. Использование копии мьютекса
type MyStruct struct {
mu sync.Mutex
}

func bad2(s MyStruct) { // Копия структуры = копия мьютекса!
s.mu.Lock()
defer s.mu.Unlock()
}

func good2(s *MyStruct) { // Указатель = один мьютекс
s.mu.Lock()
defer s.mu.Unlock()
}

Вывод: Race condition возникает при несинхронизированном доступе к разделяемым данным. В Go для предотвращения используются мьютексы, атомарные операции, каналы и sync.Map. Всегда используйте -race флаг при тестировании конкурентного кода.

Вопрос 12. Как бороться с deadlock и race condition?

Таймкод: 00:25:57

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

Правильный ответ:

Стратегии борьбы с race condition:

1. Взаимное исключение (Mutual Exclusion):

package main

import (
"fmt"
"sync"
)

// Mutex для защиты разделяемых данных
type SafeCounter struct {
mu sync.Mutex
value int
}

func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}

func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}

func main() {
counter := SafeCounter{}
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}

wg.Wait()
fmt.Println("Counter:", counter.Value()) // Всегда 1000
}

2. RWMutex для оптимизации чтения:

package main

import (
"fmt"
"sync"
)

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

func NewCache() *Cache {
return &Cache{
items: make(map[string]string),
}
}

func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // Множество читателей одновременно
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}

func (c *Cache) Set(key, value string) {
c.mu.Lock() // Эксклюзивный доступ для записи
defer c.mu.Unlock()
c.items[key] = value
}

func main() {
cache := NewCache()
var wg sync.WaitGroup

// Множество читателей
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
cache.Get("key")
}(i)
}

// Несколько писателей
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
cache.Set(fmt.Sprintf("key%d", n), "value")
}(i)
}

wg.Wait()
}

3. Атомарные операции:

package main

import (
"fmt"
"sync"
"sync/atomic"
)

func main() {
var counter int64
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}

wg.Wait()
fmt.Println("Counter:", atomic.LoadInt64(&counter))
}

4. Каналы (Share by communicating):

package main

import "fmt"

// Паттерн: одна горутина владеет данными
type Counter struct {
value int
ops chan int
done chan struct{}
}

func NewCounter() *Counter {
c := &Counter{
ops: make(chan int, 100),
done: make(chan struct{}),
}
go c.run()
return c
}

func (c *Counter) run() {
for {
select {
case op := <-c.ops:
c.value += op
case <-c.done:
return
}
}
}

func (c *Counter) Increment() {
c.ops <- 1
}

func (c *Counter) Decrement() {
c.ops <- -1
}

func (c *Counter) Value() int {
result := make(chan int, 1)
c.ops <- 0 // Специальная операция для получения значения
// В реальном коде нужен механизм возврата значения
return <-result
}

func (c *Counter) Stop() {
close(c.done)
}

func main() {
counter := NewCounter()

for i := 0; i < 1000; i++ {
go counter.Increment()
}

// Даём время на обработку
// В реальном коде нужна синхронизация
}

Стратегии борьбы с deadlock:

1. Упорядочивание блокировок:

package main

import (
"fmt"
"sync"
)

type Account struct {
ID int
Balance int
mu sync.Mutex
}

// Всегда блокируем в порядке ID
func transfer(from, to *Account, amount int) {
first, second := from, to
if from.ID > to.ID {
first, second = to, from
}

first.mu.Lock()
defer first.mu.Unlock()

second.mu.Lock()
defer second.mu.Unlock()

from.Balance -= amount
to.Balance += amount
}

func main() {
acc1 := &Account{ID: 1, Balance: 1000}
acc2 := &Account{ID: 2, Balance: 1000}

var wg sync.WaitGroup

for i := 0; i < 100; i++ {
wg.Add(2)
go func() {
defer wg.Done()
transfer(acc1, acc2, 10)
}()
go func() {
defer wg.Done()
transfer(acc2, acc1, 10)
}()
}

wg.Wait()
fmt.Printf("Account 1: %d, Account 2: %d\n", acc1.Balance, acc2.Balance)
}

2. Таймауты на блокировки:

package main

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

func tryLockWithTimeout(mu *sync.Mutex, timeout time.Duration) bool {
ch := make(chan struct{}, 1)
go func() {
mu.Lock()
ch <- struct{}{}
}()

select {
case <-ch:
return true
case <-time.After(timeout):
return false
}
}

func worker(ctx context.Context, mu1, mu2 *sync.Mutex, id int) error {
// Попытка захватить первый мьютекс
if !tryLockWithTimeout(mu1, 100*time.Millisecond) {
return fmt.Errorf("worker %d: timeout on mu1", id)
}
defer mu1.Unlock()

// Попытка захватить второй мьютекс
if !tryLockWithTimeout(mu2, 100*time.Millisecond) {
return fmt.Errorf("worker %d: timeout on mu2", id)
}
defer mu2.Unlock()

// Критическая секция
time.Sleep(10 * time.Millisecond)
return nil
}

func main() {
mu1 := &sync.Mutex{}
mu2 := &sync.Mutex{}

ctx := context.Background()

for i := 0; i < 10; i++ {
go func(id int) {
err := worker(ctx, mu1, mu2, id)
if err != nil {
fmt.Println("Error:", err)
}
}(i)
}

time.Sleep(1 * time.Second)
}

3. Использование sync.Once для инициализации:

package main

import (
"fmt"
"sync"
)

type Database struct {
connection string
}

var (
db *Database
dbOnce sync.Once
)

func GetDB() *Database {
dbOnce.Do(func() {
// Выполнится только один раз
db = &Database{connection: "connected"}
fmt.Println("Database initialized")
})
return db
}

func main() {
var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
d := GetDB()
fmt.Println(d.connection)
}()
}

wg.Wait()
}

4. Lock-free структуры данных:

package main

import (
"fmt"
"sync/atomic"
)

type LockFreeStack struct {
head unsafe.Pointer
size int64
}

type Node struct {
value int
next *Node
}

func (s *LockFreeStack) Push(value int) {
newNode := &Node{value: value}
for {
oldHead := atomic.LoadPointer(&s.head)
newNode.next = (*Node)(oldHead)
if atomic.CompareAndSwapPointer(&s.head, oldHead, unsafe.Pointer(newNode)) {
atomic.AddInt64(&s.size, 1)
return
}
}
}

func (s *LockFreeStack) Pop() (int, bool) {
for {
oldHead := atomic.LoadPointer(&s.head)
if oldHead == nil {
return 0, false
}
newHead := (*Node)(oldHead).next
if atomic.CompareAndSwapPointer(&s.head, oldHead, newHead) {
atomic.AddInt64(&s.size, -1)
return (*Node)(oldHead).value, true
}
}
}

Общие рекомендации:

1. Принцип единственного владельца данных:

// Плохо: данные доступны из разных горутин
var sharedData map[string]int

// Хорошо: данные принадлежат одной горутине
type DataStore struct {
data map[string]int
ops chan Operation
}

2. Минимизация критической секции:

// Плохо: много работы внутри блокировки
func (s *Service) Process() {
s.mu.Lock()
defer s.mu.Unlock()

data := s.prepareData() // Долго
result := s.compute(data) // Долго
s.saveResult(result) // Долго
}

// Хорошо: только необходимое внутри блокировки
func (s *Service) Process() {
data := s.prepareData() // Без блокировки
result := s.compute(data) // Без блокировки

s.mu.Lock()
s.saveResult(result) // Только запись под блокировкой
s.mu.Unlock()
}

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

# Обнаружение race condition
go run -race main.go
go test -race ./...

# Профилирование блокировок
go test -mutexprofile=mutex.pprof
go tool pprof mutex.pprof

Вывод: Для борьбы с race condition используются мьютексы, атомарные операции и каналы. Для предотвращения deadlock — упорядочивание блокировок, таймауты и lock-free структуры. В Go предпочтительный подход — "share by communicating" через каналы.

Вопрос 13. Чем отличается контейнеризация от виртуализации?

Таймкод: 00:26:39

Ответ собеседования: Правильный. Виртуализация — запуск полноценной виртуальной машины с собственной операционной системой, требует больше ресурсов. Контейнеризация — запуск процессов внутри одного ядра ОС, контейнеры стартуют быстрее, занимают меньше памяти, так как используют общее ядро.

Правильный ответ:

Определения:

Виртуализация — технология, позволяющая запускать несколько изолированных операционных систем (гостевых ОС) на одном физическом сервере с помощью гипервизора.

Контейнеризация — технология изоляции процессов в рамках одной операционной системы, где контейнеры используют общее ядро хостовой ОС.

Архитектура виртуализации:

┌─────────────────────────────────────────────┐
│ Физический сервер │
├─────────────────────────────────────────────┤
│ VM1 │ VM2 │ VM3 │
│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │
│ │ App │ │ │ App │ │ │ App │ │
│ │ БД │ │ │ Web │ │ │ Cache │ │
│ ├─────────┤ │ ├─────────┤ │ ├─────────┤ │
│ │ Guest OS│ │ │ Guest OS│ │ │ Guest OS│ │
│ └─────────┘ │ └─────────┘ │ └─────────┘ │
├─────────────────────────────────────────────┤
│ Гипервизор (Hypervisor) │
├─────────────────────────────────────────────┤
│ Host OS │
└─────────────────────────────────────────────┘

Архитектура контейнеризации:

┌─────────────────────────────────────────────┐
│ Физический сервер │
├─────────────────────────────────────────────┤
│ Container1 │ Container2 │ Container3 │
│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │
│ │ App │ │ │ App │ │ │ App │ │
│ │ БД │ │ │ Web │ │ │ Cache │ │
│ │ libs │ │ │ libs │ │ │ libs │ │
│ └─────────┘ │ └─────────┘ │ └─────────┘ │
├─────────────────────────────────────────────┤
│ Docker Engine / containerd │
├─────────────────────────────────────────────┤
│ Host OS (одно ядро) │
└─────────────────────────────────────────────┘

Сравнение характеристик:

ХарактеристикаВиртуализацияКонтейнеризация
ИзоляцияПолная (отдельное ядро)Процессная (namespaces, cgroups)
ВесГигабайтыМегабайты
ЗапускМинутыСекунды
ПроизводительностьНакладные расходы на эмуляциюПочти нативная
ПлотностьДесятки на сервереСотни-тысячи на сервере
ОСЛюбая гостевая ОСТолько ОС с тем же ядром
БезопасностьВысокаяСредняя

Механизмы изоляции в контейнерах (Linux):

1. Namespaces — изоляция ресурсов:

// Программно создать namespace (упрощённо)
package main

import (
"fmt"
"os"
"os/exec"
"syscall"
)

func main() {
cmd := exec.Command("/bin/bash")

// Устанавливаем новые namespaces
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | // Hostname
syscall.CLONE_NEWPID | // PID namespace
syscall.CLONE_NEWNS | // Mount namespace
syscall.CLONE_NEWNET | // Network namespace
syscall.CLONE_NEWUSER, // User namespace
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

2. Cgroups — ограничение ресурсов:

// Ограничение CPU и памяти через cgroups
package main

import (
"fmt"
"os"
"os/exec"
"syscall"
)

func main() {
cmd := exec.Command("/bin/bash")

cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
}

// Ограничения задаются через cgroup filesystem
// /sys/fs/cgroup/memory/container1/memory.limit_in_bytes
// /sys/fs/cgroup/cpu/container1/cpu.shares

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

cmd.Run()
}

Пример Dockerfile:

# Многоступенчатая сборка для Go приложения
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server .

# Минимальный образ для запуска
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/
COPY --from=builder /server .

EXPOSE 8080

CMD ["./server"]

Пример docker-compose.yml:

version: '3.8'

services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M

postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
resources:
limits:
memory: 1G

redis:
image: redis:7-alpine
deploy:
resources:
limits:
memory: 256M

volumes:
postgres_data:

Когда использовать виртуализацию:

// Сценарии для виртуализации:
// 1. Разные ОС (Windows + Linux)
// 2. Высокие требования к безопасности
// 3. Устаревшие приложения с зависимостью от конкретного ядра
// 4. Разработка и тестирование ядра ОС

Когда использовать контейнеризацию:

// Сценарии для контейнеризации:
// 1. Микросервисная архитектура
// 2. CI/CD пайплайны
// 3. Масштабирование приложений
// 4. Разработка и тестирование
// 5. Облачные приложения

Гибридный подход (Kubernetes):

# Kubernetes объединяет оба подхода
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10

Производительность:

Тест: Запуск 100 инстансов приложения

Виртуализация (KVM):
- Время запуска: ~5 минут
- Память: ~50 ГБ (500 MB × 100)
- CPU overhead: ~10%

Контейнеризация (Docker):
- Время запуска: ~30 секунд
- Память: ~10 ГБ (100 MB × 100)
- CPU overhead: ~1-2%

Безопасность:

Виртуализация:
✓ Полная изоляция
✓ Различные ядра ОС
✓ Аппаратная виртуализация
✓ Сложнее выйти за пределы VM

Контейнеризация:
△ Общее ядро ОС
△ Уязвимости ядра влияют на все контейнеры
△ Нужны дополнительные меры (seccomp, AppArmor)
✓ Быстрое развёртывание патчей

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

Вопрос 14. Что такое индексация в базах данных и для чего она используется?

Таймкод: 00:27:43

Ответ собеседника: Правильный. Индексация — это добавление дополнительных структур данных (например, B-tree индекс) поверх таблицы для ускорения поиска. Без индекса поиск O(N), с индексом — O(log N). Недостаток — дополнительный расход памяти.

Правильный ответ:

Определение:

Индекс — это структура данных, которая ускоряет операции поиска строк в таблице базы данных. Индекс создаёт отсортированное представление данных по указанным столбцам, позволяя СУБД быстро находить нужные строки без полного сканирования таблицы.

Аналогия:

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

Типы индексов:

1. B-Tree индекс (самый распространённый):

[50]
/ \
[20|40] [60|80]
/ | \ / | \
[10] [30] [45] [55] [70] [90]
-- Создание B-Tree индекса
CREATE INDEX idx_users_email ON users(email);

-- Составной индекс
CREATE INDEX idx_users_name_email ON users(last_name, first_name);

-- Использование
SELECT * FROM users WHERE email = 'user@example.com';
-- Без индекса: Seq Scan O(N)
-- С индексом: Index Scan O(log N)

2. Hash индекс:

-- Hash индекс (только для точного совпадения)
CREATE INDEX idx_users_email_hash ON users USING hash(email);

-- Работает для:
SELECT * FROM users WHERE email = 'user@example.com';

-- НЕ работает для:
SELECT * FROM users WHERE email LIKE '%example%';
SELECT * FROM users WHERE email > 'a';

3. GiST (Generalized Search Tree):

-- Для географических данных
CREATE INDEX idx_locations_gist ON locations USING gist(coordinates);

-- Для полнотекстового поиска
CREATE INDEX idx_documents_content ON documents USING gist(to_tsvector('english', content));

4. GIN (Generalized Inverted Index):

-- Для массивов и JSONB
CREATE INDEX idx_products_tags ON products USING gin(tags);

-- Для JSONB
CREATE INDEX idx_users_data ON users USING gin(data);

-- Пример запроса
SELECT * FROM users WHERE data @> '{"role": "admin"}';

Сравнение типов индексов:

ТипЛучше всего дляСложность поиска
B-TreeДиапазонные запросы, сортировкаO(log N)
HashТочное совпадениеO(1)
GiSTГеоданные, полнотекстовый поискO(log N)
GINМассивы, JSONB, полнотекстовый поискO(log N)

Примеры создания индексов:

-- Простой индекс
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- Уникальный индекс
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- Частичный индекс (индексирует только часть данных)
CREATE INDEX idx_active_users ON users(last_login)
WHERE active = true;

-- Выражение индекс
CREATE INDEX idx_users_lower_email ON users(lower(email));

-- Индекс для сортировки
CREATE INDEX idx_orders_created_desc ON orders(created_at DESC);

-- Покрывающий индекс (covering index)
CREATE INDEX idx_orders_covering ON orders(customer_id)
INCLUDE (total, status);

Анализ использования индексов:

-- Просмотр плана выполнения
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';

-- Результат без индекса:
-- Seq Scan on users (cost=0.00..10000.00 rows=1 width=100)
-- Filter: (email = 'user@example.com')
-- Rows Removed by Filter: 999999
-- Planning Time: 0.1 ms
-- Execution Time: 500.0 ms

-- Результат с индексом:
-- Index Scan using idx_users_email on users (cost=0.42..8.44 rows=1 width=100)
-- Index Cond: (email = 'user@example.com')
-- Planning Time: 0.2 ms
-- Execution Time: 0.1 ms

Просмотр существующих индексов:

-- PostgreSQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'users';

-- Размер индексов
SELECT
indexrelname AS index_name,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS times_used
FROM pg_stat_user_indexes
WHERE relname = 'users'
ORDER BY pg_relation_size(indexrelid) DESC;

Когда индексы НЕ помогают:

-- 1. Маленькие таблицы (< 1000 строк)
-- Полное сканирование быстрее

-- 2. Высокая селективность (мало уникальных значений)
CREATE INDEX idx_users_gender ON users(gender); -- Плохая идея (M/F)

-- 3. Частые обновления
-- Индекс замедляет INSERT/UPDATE/DELETE

-- 4. Использование функций на индексированном столбце
SELECT * FROM users WHERE lower(email) = 'user@example.com';
-- Решение: CREATE INDEX idx_lower_email ON users(lower(email));

Оптимизация запросов с индексами:

-- Плохо: индекс не используется
SELECT * FROM orders WHERE YEAR(created_at) = 2023;

-- Хорошо: индекс используется
SELECT * FROM orders
WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';

-- Плохо: OR может не использовать индекс
SELECT * FROM users WHERE email = 'a@b.com' OR phone = '123456';

-- Хорошо: UNION
SELECT * FROM users WHERE email = 'a@b.com'
UNION
SELECT * FROM users WHERE phone = '123456';

Мониторинг использования индексов:

-- Неиспользуемые индексы (кандидаты на удаление)
SELECT
schemaname || '.' || relname AS table,
indexrelname AS index,
pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
idx_scan as index_scans
FROM pg_stat_user_indexes ui
JOIN pg_index i ON ui.indexrelid = i.indexrelid
WHERE idx_scan = 0
AND NOT indisunique
ORDER BY pg_relation_size(i.indexrelid) DESC;

Пример на Go с использованием индексов:

package main

import (
"context"
"database/sql"
"fmt"
"time"

_ "github.com/lib/pq"
)

type UserStore struct {
db *sql.DB
}

func NewUserStore(connStr string) (*UserStore, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

return &UserStore{db: db}, nil
}

func (s *UserStore) CreateIndexes() error {
queries := []string{
`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`,
`CREATE INDEX IF NOT EXISTS idx_users_created ON users(created_at DESC)`,
`CREATE INDEX IF NOT EXISTS idx_orders_customer ON orders(customer_id)`,
}

for _, query := range queries {
if _, err := s.db.Exec(query); err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
}
return nil
}

func (s *UserStore) GetUserByEmail(ctx context.Context, email string) (*User, error) {
// Этот запрос использует idx_users_email
query := `SELECT id, email, name, created_at FROM users WHERE email = $1`

var user User
err := s.db.QueryRowContext(ctx, query, email).Scan(
&user.ID, &user.Email, &user.Name, &user.CreatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}

func (s *UserStore) GetRecentUsers(ctx context.Context, limit int) ([]User, error) {
// Этот запрос использует idx_users_created
query := `
SELECT id, email, name, created_at
FROM users
ORDER BY created_at DESC
LIMIT $1
`

rows, err := s.db.QueryContext(ctx, query, limit)
if err != nil {
return nil, err
}
defer rows.Close()

var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email, &u.Name, &u.CreatedAt); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}

Вывод: Индексы критически важны для производительности баз данных. B-Tree — универсальный выбор, Hash — для точного совпадения, GIN/GiST — для специализированных типов данных. Важно мониторить использование индексов и удалять неиспользуемые.

Вопрос 15. Что такое транзакции в реляционных базах данных?

Таймкод: 00:28:46

Ответ собеседника: Правильный. Транзакция — это логическая группа операций, которые выполняются атомарно (неделимо). Либо все операции выполняются полностью, либо при ошибке откатываются (rollback). Например, если из трёх операций первые две выполнились, а третья нет — первые две откатываются.

Правильный ответ:

Определение:

Транзакция — это логическая единица работы с базой данных, которая объединяет одну или несколько операций в неделимую последовательность. Транзакция либо выполняется целиком (commit), либо полностью отменяется (rollback).

ACID свойства транзакций:

A — Atomicity (Атомарность): Все операции транзакции выполняются как единое целое. Либо все успешны, либо ни одна.

C — Consistency (Согласованность): Транзакция переводит базу данных из одного согласованного состояния в другое.

I — Isolation (Изоляция): Параллельные транзакции не влияют друг на друга.

D — Durability (Долговечность): После фиксации транзакции изменения сохраняются навсегда.

Пример транзакции — перевод денег:

-- Без транзакции (ПЛОХО)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Если здесь произойдёт сбой, деньги списаны, но не зачислены!
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- С транзакцией (ПРАВИЛЬНО)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Если любая операция завершится ошибкой, обе откатятся

Реализация на Go:

package main

import (
"context"
"database/sql"
"fmt"
"time"

_ "github.com/lib/pq"
)

type AccountService struct {
db *sql.DB
}

func NewAccountService(db *sql.DB) *AccountService {
return &AccountService{db: db}
}

// Transfer выполняет перевод между счетами в транзакции
func (s *AccountService) Transfer(ctx context.Context, fromID, toID int, amount float64) error {
// Начинаем транзакцию
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}

// Гарантируем rollback при ошибке
defer func() {
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
fmt.Printf("rollback failed: %v\n", rbErr)
}
}
}()

// Проверяем баланс отправителя
var balance float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
fromID,
).Scan(&balance)
if err != nil {
return fmt.Errorf("failed to get balance: %w", err)
}

if balance < amount {
return fmt.Errorf("insufficient balance")
}

// Списываем средства
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID,
)
if err != nil {
return fmt.Errorf("failed to debit: %w", err)
}

// Зачисляем средства
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID,
)
if err != nil {
return fmt.Errorf("failed to credit: %w", err)
}

// Фиксируем транзакцию
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit: %w", err)
}

return nil
}

Уровни изоляции транзакций:

-- Read Uncommitted (самый слабый)
-- Можно читать незафиксированные данные
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Read Committed (по умолчанию в PostgreSQL)
-- Читает только зафиксированные данные
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Repeatable Read
-- Гарантирует, что повторное чтение вернёт те же данные
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Serializable (самый строгий)
-- Полная изоляция, как если бы транзакции выполнялись последовательно
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Проблемы параллельного доступа:

1. Dirty Read (Грязное чтение):

Транзакция 1 Транзакция 2
│ │
├── UPDATE x = 100 │
│ ├── SELECT x (видит 100!)
│ │
├── ROLLBACK │
│ │
│ │ ← Прочитала данные, которых нет

2. Non-Repeatable Read:

Транзакция 1 Транзакция 2
│ │
├── SELECT x (100) │
│ ├── UPDATE x = 200
│ ├── COMMIT
├── SELECT x (200!) │
│ │
│ ← Разные результаты │

3. Phantom Read:

Транзакция 1 Транзакция 2
│ │
├── SELECT WHERE age > 20 │
│ (5 строк) │
│ ├── INSERT age = 25
│ ├── COMMIT
├── SELECT WHERE age > 20 │
│ (6 строк!) │
│ │
│ ← Появились фантомы │

Сравнение уровней изоляции:

УровеньDirty ReadNon-RepeatablePhantom
Read Uncommitted
Read Committed
Repeatable Read✗*
Serializable

*В PostgreSQL Repeatable Read также предотвращает phantom reads

Реализация на Go с разными уровнями изоляции:

package main

import (
"context"
"database/sql"
"fmt"
)

type TransactionService struct {
db *sql.DB
}

// ReadCommittedExample — чтение только зафиксированных данных
func (s *TransactionService) ReadCommittedExample(ctx context.Context, accountID int) error {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return err
}
defer tx.Rollback()

var balance float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1",
accountID,
).Scan(&balance)
if err != nil {
return err
}

fmt.Printf("Balance: %.2f\n", balance)
return tx.Commit()
}

// RepeatableReadExample — повторное чтение вернёт те же данные
func (s *TransactionService) RepeatableReadExample(ctx context.Context, accountID int) error {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return err
}
defer tx.Rollback()

// Первое чтение
var balance1 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1",
accountID,
).Scan(&balance1)
if err != nil {
return err
}

// ... другая транзакция может изменить данные ...

// Второе чтение — вернёт то же значение
var balance2 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1",
accountID,
).Scan(&balance2)
if err != nil {
return err
}

// balance1 == balance2 гарантировано
fmt.Printf("Balance1: %.2f, Balance2: %.2f\n", balance1, balance2)
return tx.Commit()
}

// SerializableExample — полная изоляция
func (s *TransactionService) SerializableExample(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return err
}
defer tx.Rollback()

// Все операции изолированы от других транзакций
var total float64
err = tx.QueryRowContext(ctx,
"SELECT SUM(balance) FROM accounts",
).Scan(&total)
if err != nil {
return err
}

fmt.Printf("Total balance: %.2f\n", total)
return tx.Commit()
}

Savepoints — точки сохранения внутри транзакции:

func (s *TransactionService) TransferWithSavepoint(ctx context.Context, fromID, toID int, amount float64) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

// Создаём точку сохранения
_, err = tx.ExecContext(ctx, "SAVEPOINT before_debit")
if err != nil {
return err
}

// Списываем средства
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID,
)
if err != nil {
// Откатываемся к точке сохранения
_, _ = tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT before_debit")
return fmt.Errorf("debit failed: %w", err)
}

// Зачисляем средства
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID,
)
if err != nil {
return fmt.Errorf("credit failed: %w", err)
}

return tx.Commit()
}

Паттерн Unit of Work:

type UnitOfWork struct {
tx *sql.Tx
ctx context.Context
commits []func() error
rollbacks []func() error
}

func NewUnitOfWork(ctx context.Context, db *sql.DB) (*UnitOfWork, error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
return &UnitOfWork{tx: tx, ctx: ctx}, nil
}

func (uow *UnitOfWork) RegisterCommit(fn func() error) {
uow.commits = append(uow.commits, fn)
}

func (uow *UnitOfWork) RegisterRollback(fn func() error) {
uow.rollbacks = append(uow.rollbacks, fn)
}

func (uow *UnitOfWork) Commit() error {
if err := uow.tx.Commit(); err != nil {
return err
}
for _, fn := range uow.commits {
if err := fn(); err != nil {
return err
}
}
return nil
}

func (uow *UnitOfWork) Rollback() error {
for _, fn := range uow.rollbacks {
_ = fn()
}
return uow.tx.Rollback()
}

Вывод: Транзакции обеспечивают целостность данных через ACID свойства. Выбор уровня изоляции зависит от требований к согласованности и производительности. В Go транзакции реализуются через database/sql с явным управлением commit/rollback.

Вопрос 16. Какие уровни изоляции транзакций существуют и в чём их разница?

Таймкод: 00:29:27

Ответ собеседника: Правильный. Read uncommitted — чтение даже неподтверждённых изменений (грязное чтение), в PostgreSQL недоступен. Read committed — чтение только закоммиченных данных, базовое поведение в PostgreSQL. Repeatable read — данные сохраняются на момент старта транзакции, параллельная транзакция не увидит изменения после коммита. Serializable — полное исключение параллельного выполнения транзакций, они выполняются последовательно.

Правильный ответ:

Уровни изоляции транзакций:

Уровни изоляции определяют, как транзакции взаимодействуют друг с другом при параллельном выполнении. Стандарт SQL определяет четыре уровня, от самого слабого к самому строгому.

1. Read Uncommitted (Чтение незафиксированных данных)

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

Транзакция A Транзакция B
│ │
│ ├── BEGIN
│ ├── UPDATE x = 100
├── BEGIN │
├── SELECT x → 100 │
│ (грязное чтение!) │
│ ├── ROLLBACK
├── SELECT x → 0 │
│ (данные откатились!) │
-- Установка уровня
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- В PostgreSQL этот уровень эквивалентен READ COMMITTED
-- так как PostgreSQL не допускает грязное чтение

Проблемы:

  • Dirty Read (грязное чтение)
  • Non-Repeatable Read
  • Phantom Read

2. Read Committed (Чтение зафиксированных данных)

Уровень по умолчанию в PostgreSQL. Транзакция читает только зафиксированные данные. Каждый запрос внутри транзакции видит последнее зафиксированное состояние.

Транзакция A Транзакция B
│ │
├── BEGIN │
├── SELECT x → 100 │
│ ├── BEGIN
│ ├── UPDATE x = 200
│ ├── COMMIT
├── SELECT x → 200 │
│ (non-repeatable read!) │
├── COMMIT │
-- Установка уровня
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Пример
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 100
-- Другая транзакция обновляет и фиксирует
SELECT balance FROM accounts WHERE id = 1; -- 200 (изменилось!)
COMMIT;

Проблемы:

  • Non-Repeatable Read
  • Phantom Read

3. Repeatable Read (Повторяемое чтение)

Гарантирует, что данные, прочитанные в транзакции, не изменятся до её завершения. Повторный запрос вернёт те же данные.

Транзакция A Транзакция B
│ │
├── BEGIN │
├── SELECT x → 100 │
│ ├── BEGIN
│ ├── UPDATE x = 200
│ ├── COMMIT
├── SELECT x → 100 │
│ (те же данные!) │
├── COMMIT │
-- Установка уровня
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Пример
BEGIN;
SELECT * FROM accounts WHERE balance > 1000; -- 5 строк
-- Другая транзакция добавляет новую строку
SELECT * FROM accounts WHERE balance > 1000; -- всё ещё 5 строк
COMMIT;

Особенности в PostgreSQL:

  • Предотвращает phantom reads через механизм MVCC
  • Первая транзакция, которая обновляет строку, выигрывает
  • Вторая получит ошибку serialization failure

Проблемы:

  • Serialization anomalies (в некоторых СУБД)

4. Serializable (Сериализуемый)

Самый строгий уровень. Транзакции выполняются так, как если бы они шли последовательно, одна за другой.

Транзакция A Транзакция B
│ │
├── BEGIN │
├── SELECT x → 100 │
│ ├── BEGIN
│ ├── SELECT x → 100
├── UPDATE x = 200 │
├── COMMIT │
│ ├── UPDATE x = 300
│ ├── ERROR!
│ │ (serialization failure)
-- Установка уровня
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Пример с обработкой ошибок
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- Операции...
COMMIT;
-- Если получили ошибку, нужно повторить транзакцию

Сравнительная таблица:

УровеньDirty ReadNon-Repeatable ReadPhantom ReadПроизводительность
Read UncommittedВысокая
Read CommittedВысокая
Repeatable Read✓*Средняя
SerializableНизкая

*В PostgreSQL Repeatable Read предотвращает phantom reads

Реализация на Go с обработкой ошибок:

package main

import (
"context"
"database/sql"
"fmt"
"time"

"github.com/lib/pq"
)

type IsolationService struct {
db *sql.DB
}

func NewIsolationService(db *sql.DB) *IsolationService {
return &IsolationService{db: db}
}

// ReadCommittedExample — пример с Read Committed
func (s *IsolationService) ReadCommittedExample(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback()

// Первое чтение
var balance1 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1", 1,
).Scan(&balance1)
if err != nil {
return err
}
fmt.Printf("First read: %.2f\n", balance1)

// Другая транзакция может изменить данные здесь

// Второе чтение может вернуть другое значение
var balance2 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1", 1,
).Scan(&balance2)
if err != nil {
return err
}
fmt.Printf("Second read: %.2f\n", balance2)

return tx.Commit()
}

// RepeatableReadExample — пример с Repeatable Read
func (s *IsolationService) RepeatableReadExample(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback()

// Первое чтение
var balance1 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1", 1,
).Scan(&balance1)
if err != nil {
return err
}
fmt.Printf("First read: %.2f\n", balance1)

// Другая транзакция может изменить данные,
// но мы увидим те же данные

// Второе чтение вернёт то же значение
var balance2 float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1", 1,
).Scan(&balance2)
if err != nil {
return err
}
fmt.Printf("Second read: %.2f\n", balance2)
// balance1 == balance2 гарантировано

return tx.Commit()
}

// SerializableWithRetry — Serializable с повторными попытками
func (s *IsolationService) SerializableWithRetry(ctx context.Context, fn func(*sql.Tx) error) error {
maxRetries := 3

for attempt := 0; attempt < maxRetries; attempt++ {
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}

err = fn(tx)
if err != nil {
tx.Rollback()

// Проверяем, это serialization failure?
if isSerializationError(err) {
fmt.Printf("Serialization conflict, retry %d/%d\n", attempt+1, maxRetries)
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
continue
}
return err
}

err = tx.Commit()
if err != nil {
if isSerializationError(err) {
fmt.Printf("Serialization conflict on commit, retry %d/%d\n", attempt+1, maxRetries)
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
continue
}
return err
}

return nil // Успех
}

return fmt.Errorf("max retries exceeded")
}

func isSerializationError(err error) bool {
if err == nil {
return false
}
// PostgreSQL serialization failure error code
if pqErr, ok := err.(*pq.Error); ok {
return pqErr.Code == "40001" // serialization_failure
}
return false
}

// Пример использования Serializable с retry
func (s *IsolationService) TransferMoney(ctx context.Context, fromID, toID int, amount float64) error {
return s.SerializableWithRetry(ctx, func(tx *sql.Tx) error {
var fromBalance float64
err := tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1", fromID,
).Scan(&fromBalance)
if err != nil {
return err
}

if fromBalance < amount {
return fmt.Errorf("insufficient balance")
}

_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID,
)
if err != nil {
return err
}

_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID,
)
if err != nil {
return err
}

return nil
})
}

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

// Read Committed — для большинства операций
// - Чтение данных для отображения
// - Простые обновления
// - Высокая конкурентность

// Repeatable Read — когда нужна консистентность данных
// - Отчёты
// - Сложные вычисления
// - Проверки перед обновлением

// Serializable — для критичных операций
// - Финансовые транзакции
// - Бронирование ресурсов
// - Когда недопустимы аномалии

Вывод: Выбор уровня изоляции — это компромисс между согласованностью данных и производительностью. Read Committed подходит для большинства случаев. Serializable обеспечивает максимальную изоляцию, но требует обработки ошибок и повторных попыток.

Вопрос 17. В чём ключевые отличия реляционных баз данных от NoSQL?

Таймкод: 00:31:18

Ответ собеседника: Правильный. Реляционные БД гарантируют ACID (атомарность, консистентность, изоляция, долговечность), имеют строгую схему, строгие связи между таблицами. NoSQL используют модель BASE (basic available, soft state, eventually consistent), хранят данные как документы (JSON), не имеют строгой типизации. Реляционные лучше, когда известна структура данных и нужны связи. NoSQL — когда данные неопределённые или часто меняются.

Правильный ответ:

ACID vs BASE:

Реляционные базы данных (ACID):

СвойствоОписание
AtomicityТранзакция выполняется целиком или не выполняется
ConsistencyДанные всегда в согласованном состоянии
IsolationПараллельные транзакции не влияют друг на друга
DurabilityЗафиксированные данные сохраняются навсегда

NoSQL базы данных (BASE):

СвойствоОписание
Basically AvailableСистема доступна большую часть времени
Soft StateСостояние может меняться без ввода
Eventually ConsistentДанные станут согласованными со временем

Типы NoSQL баз данных:

1. Документные (MongoDB, CouchDB):

// Пример документа в MongoDB
{
"_id": ObjectId("..."),
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"country": "USA"
},
"orders": [
{"id": 1, "total": 100},
{"id": 2, "total": 200}
]
}
// Пример работы с MongoDB в Go
package main

import (
"context"
"fmt"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

type User struct {
Name string `bson:"name"`
Email string `bson:"email"`
Address struct {
Street string `bson:"street"`
City string `bson:"city"`
Country string `bson:"country"`
} `bson:"address"`
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
panic(err)
}
defer client.Disconnect(ctx)

collection := client.Database("mydb").Collection("users")

// Вставка документа
user := User{
Name: "John Doe",
Email: "john@example.com",
}
user.Address.Street = "123 Main St"
user.Address.City = "New York"
user.Address.Country = "USA"

result, err := collection.InsertOne(ctx, user)
if err != nil {
panic(err)
}
fmt.Println("Inserted ID:", result.InsertedID)

// Поиск
var foundUser User
err = collection.FindOne(ctx, bson.M{"email": "john@example.com"}).Decode(&foundUser)
if err != nil {
panic(err)
}
fmt.Printf("Found: %+v\n", foundUser)
}

2. Ключ-значение (Redis, Memcached):

package main

import (
"context"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

func main() {
ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})

// Установка значения
err := rdb.Set(ctx, "user:1:name", "John Doe", 1*time.Hour).Err()
if err != nil {
panic(err)
}

// Получение значения
val, err := rdb.Get(ctx, "user:1:name").Result()
if err != nil {
panic(err)
}
fmt.Println("Name:", val)

// Работа с хешами
rdb.HSet(ctx, "user:1", map[string]interface{}{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
})

user, err := rdb.HGetAll(ctx, "user:1").Result()
if err != nil {
panic(err)
}
fmt.Printf("User: %+v\n", user)

// Списки
rdb.LPush(ctx, "queue:tasks", "task1", "task2", "task3")
task, err := rdb.RPop(ctx, "queue:tasks").Result()
if err != nil {
panic(err)
}
fmt.Println("Task:", task)

// Множества
rdb.SAdd(ctx, "tags", "golang", "redis", "nosql")
tags, err := rdb.SMembers(ctx, "tags").Result()
if err != nil {
panic(err)
}
fmt.Println("Tags:", tags)
}

3. Колоночные (Cassandra, HBase):

-- Пример схемы в Cassandra
CREATE TABLE user_activities (
user_id UUID,
activity_date DATE,
activity_time TIMESTAMP,
activity_type TEXT,
details TEXT,
PRIMARY KEY ((user_id, activity_date), activity_time)
) WITH CLUSTERING ORDER BY (activity_time DESC);

4. Графовые (Neo4j, ArangoDB):

-- Пример запроса в Neo4j
CREATE (john:Person {name: 'John', age: 30})
CREATE (jane:Person {name: 'Jane', age: 28})
CREATE (john)-[:FRIEND]->(jane)

-- Поиск друзей
MATCH (p:Person {name: 'John'})-[:FRIEND]->(friend)
RETURN friend.name

Сравнительная таблица:

ХарактеристикаРеляционныеNoSQL
СхемаСтрогая, предопределённаяГибкая, динамическая
МасштабированиеВертикальноеГоризонтальное
ЗапросыSQLСпецифичные для каждой СУБД
ТранзакцииACIDBASE (обычно)
СвяжиСложные JOINДенормализация
ПримерыPostgreSQL, MySQLMongoDB, Redis, Cassandra

Когда использовать реляционные:

-- 1. Сложные запросы с JOIN
SELECT u.name, COUNT(o.id) as order_count, SUM(o.total) as total_spent
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2023-01-01'
GROUP BY u.name
HAVING COUNT(o.id) > 5
ORDER BY total_spent DESC;

-- 2. Транзакции с ACID
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
INSERT INTO transactions (from_id, to_id, amount) VALUES (1, 2, 100);
COMMIT;

-- 3. Строгая целостность данных
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
total DECIMAL(10,2) CHECK (total > 0),
status VARCHAR(20) CHECK (status IN ('pending', 'paid', 'shipped'))
);

Когда использовать NoSQL:

// 1. Высокая нагрузка на запись (Redis)
func cacheExample(rdb *redis.Client) {
ctx := context.Background()

// Кэширование сессий
rdb.Set(ctx, "session:abc123", userData, 24*time.Hour)

// Счётчик просмотров
rdb.Incr(ctx, "page:views:home")

// Rate limiting
key := fmt.Sprintf("rate_limit:%s", userID)
current, _ := rdb.Incr(ctx, key).Result()
if current == 1 {
rdb.Expire(ctx, key, time.Minute)
}
}

// 2. Гибкая схема (MongoDB)
func flexibleSchema(collection *mongo.Collection) {
ctx := context.Background()

// Документы могут иметь разную структуру
doc1 := bson.M{
"type": "user",
"name": "John",
"email": "john@example.com",
}

doc2 := bson.M{
"type": "user",
"name": "Jane",
"profile": bson.M{
"bio": "Developer",
"social": bson.M{
"twitter": "@jane",
"github": "jane",
},
},
}

collection.InsertMany(ctx, []interface{}{doc1, doc2})
}

// 3. Большие данные (Cassandra)
func timeSeriesExample(session *gocql.Session) {
// Запись метрик
session.Query(
`INSERT INTO metrics (host, timestamp, cpu, memory)
VALUES (?, ?, ?, ?)`,
"server1", time.Now(), 75.5, 82.3,
).Exec()
}

Гибридный подход (Polyglot Persistence):

// Приложение использует разные базы данных для разных задач
type Application struct {
postgres *sql.DB // Основные данные
redis *redis.Client // Кэш и сессии
mongo *mongo.Client // Логи и аналитика
}

func (app *Application) GetUser(ctx context.Context, userID int) (*User, error) {
// 1. Проверяем кэш
cacheKey := fmt.Sprintf("user:%d", userID)
cached, err := app.redis.Get(ctx, cacheKey).Result()
if err == nil {
var user User
if json.Unmarshal([]byte(cached), &user) == nil {
return &user, nil
}
}

// 2. Ищем в PostgreSQL
user, err := app.getUserFromPostgres(ctx, userID)
if err != nil {
return nil, err
}

// 3. Сохраняем в кэш
userData, _ := json.Marshal(user)
app.redis.Set(ctx, cacheKey, userData, 1*time.Hour)

// 4. Логируем в MongoDB
app.mongo.Database("logs").Collection("user_views").InsertOne(ctx, bson.M{
"user_id": userID,
"viewed_at": time.Now(),
})

return user, nil
}

Выбор базы данных:

Используйте реляционные, когда:
✓ Нужны сложные запросы с JOIN
✓ Важна целостность данных (ACID)
✓ Структура данных известна и стабильна
✓ Финансовые операции
✓ Сложная бизнес-логика

Используйте NoSQL, когда:
✓ Нужна горизонтальная масштабируемость
✓ Гибкая или меняющаяся схема
✓ Высокая нагрузка на запись
✓ Простые запросы по ключу
✓ Кэширование и сессии
✓ Большие объёмы неструктурированных данных

Вывод: Реляционные и NoSQL базы данных решают разные задачи. Реляционные обеспечивают ACID и сложные запросы, NoSQL — масштабируемость и гибкость. Современные приложения часто используют оба подхода (polyglot persistence).

Вопрос 18. Приведите примеры задач, для которых NoSQL базы данных подходят лучше.

Таймкод: 00:32:55

Ответ собеседника: Правильный. Хранение документов или неопределённых данных, например payloads от разных провайдеров (разные JSON-структуры). Также подходят для хранения логов и событий. В PostgreSQL есть тип JSONB, но для таких задач проще использовать MongoDB.

Правильный ответ:

Сценарии, где NoSQL превосходит реляционные базы данных:

1. Кэширование и хранение сессий (Redis):

package main

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

type SessionManager struct {
rdb *redis.Client
}

func NewSessionManager() *SessionManager {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
return &SessionManager{rdb: rdb}
}

func (sm *SessionManager) SetSession(ctx context.Context, sessionID string, data map[string]interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
return sm.rdb.Set(ctx, "session:"+sessionID, jsonData, 24*time.Hour).Err()
}

func (sm *SessionManager) GetSession(ctx context.Context, sessionID string) (map[string]interface{}, error) {
data, err := sm.rdb.Get(ctx, "session:"+sessionID).Result()
if err != nil {
return nil, err
}

var result map[string]interface{}
if err := json.Unmarshal([]byte(data), &result); err != nil {
return nil, err
}
return result, nil
}

// Rate limiting
func (sm *SessionManager) CheckRateLimit(ctx context.Context, userID string, maxRequests int, window time.Duration) (bool, error) {
key := fmt.Sprintf("rate_limit:%s", userID)
pipe := sm.rdb.Pipeline()

pipe.Incr(ctx, key)
pipe.Expire(ctx, key, window)

results, err := pipe.Exec(ctx)
if err != nil {
return false, err
}

count := results[0].(*redis.IntCmd).Val()
return count <= int64(maxRequests), nil
}

2. Хранение логов и событий (MongoDB, Elasticsearch):

package main

import (
"context"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

type LogEntry struct {
Timestamp time.Time `bson:"timestamp"`
Level string `bson:"level"`
Service string `bson:"service"`
Message string `bson:"message"`
Metadata map[string]interface{} `bson:"metadata"`
}

type LogStore struct {
collection *mongo.Collection
}

func NewLogStore(db *mongo.Database) *LogStore {
collection := db.Collection("logs")

// Создаём TTL индекс для автоматического удаления старых логов
collection.Indexes().CreateOne(context.Background(), mongo.IndexModel{
Keys: bson.D{{Key: "timestamp", Value: 1}},
Options: options.Index().SetExpireAfterSeconds(30 * 24 * 3600), // 30 дней
})

return &LogStore{collection: collection}
}

func (ls *LogStore) InsertLog(ctx context.Context, entry LogEntry) error {
_, err := ls.collection.InsertOne(ctx, entry)
return err
}

func (ls *LogStore) QueryLogs(ctx context.Context, service string, from, to time.Time) ([]LogEntry, error) {
filter := bson.M{
"service": service,
"timestamp": bson.M{
"$gte": from,
"$lte": to,
},
}

cursor, err := ls.collection.Find(ctx, filter)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

var logs []LogEntry
if err = cursor.All(ctx, &logs); err != nil {
return nil, err
}
return logs, nil
}

3. Реальное время и счётчики (Redis):

package main

import (
"context"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

type RealtimeStats struct {
rdb *redis.Client
}

func NewRealtimeStats() *RealtimeStats {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
return &RealtimeStats{rdb: rdb}
}

// Онлайн пользователи
func (rs *RealtimeStats) UserOnline(ctx context.Context, userID int) {
rs.rdb.SAdd(ctx, "online_users", userID)
rs.rdb.Expire(ctx, "online_users", 5*time.Minute)
}

func (rs *RealtimeStats) UserOffline(ctx context.Context, userID int) {
rs.rdb.SRem(ctx, "online_users", userID)
}

func (rs *RealtimeStats) GetOnlineCount(ctx context.Context) int64 {
return rs.rdb.SCard(ctx, "online_users").Val()
}

// Счётчик просмотров
func (rs *RealtimeStats) IncrementPageView(ctx context.Context, page string) {
today := time.Now().Format("2006-01-02")
rs.rdb.Incr(ctx, fmt.Sprintf("pageviews:%s:%s", page, today))
rs.rdb.Expire(ctx, fmt.Sprintf("pageviews:%s:%s", page, today), 48*time.Hour)
}

// Лидерборд
func (rs *RealtimeStats) UpdateScore(ctx context.Context, userID int, score float64) {
rs.rdb.ZAdd(ctx, "leaderboard", redis.Z{
Score: score,
Member: fmt.Sprintf("user:%d", userID),
})
}

func (rs *RealtimeStats) GetTopPlayers(ctx context.Context, count int) ([]redis.Z, error) {
return rs.rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, int64(count-1)).Result()
}

4. Каталог товаров с гибкой схемой (MongoDB):

package main

import (
"context"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

type Product struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Category string `bson:"category"`
Price float64 `bson:"price"`
Attributes map[string]interface{} `bson:"attributes"`
}

type ProductCatalog struct {
collection *mongo.Collection
}

func NewProductCatalog(db *mongo.Database) *ProductCatalog {
return &ProductCatalog{
collection: db.Collection("products"),
}
}

func (pc *ProductCatalog) InsertProduct(ctx context.Context, product Product) error {
_, err := pc.collection.InsertOne(ctx, product)
return err
}

// Поиск по произвольным атрибутам
func (pc *ProductCatalog) FindByAttribute(ctx context.Context, key string, value interface{}) ([]Product, error) {
filter := bson.M{fmt.Sprintf("attributes.%s", key): value}

cursor, err := pc.collection.Find(ctx, filter)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

var products []Product
if err = cursor.All(ctx, &products); err != nil {
return nil, err
}
return products, nil
}

// Пример товаров с разной структурой
func (pc *ProductCatalog) ExampleProducts(ctx context.Context) {
laptop := Product{
ID: "laptop-1",
Name: "MacBook Pro",
Category: "electronics",
Price: 1999.99,
Attributes: map[string]interface{}{
"brand": "Apple",
"cpu": "M2",
"ram_gb": 16,
"storage_gb": 512,
"screen_size_inch": 14.2,
},
}

tshirt := Product{
ID: "tshirt-1",
Name: "Cotton T-Shirt",
Category: "clothing",
Price: 29.99,
Attributes: map[string]interface{}{
"brand": "Nike",
"size": "L",
"color": "blue",
"material": "100% cotton",
},
}

pc.InsertProduct(ctx, laptop)
pc.InsertProduct(ctx, tshirt)
}

5. Очереди сообщений (Redis Streams):

package main

import (
"context"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

type MessageQueue struct {
rdb *redis.Client
}

func NewMessageQueue() *MessageQueue {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
return &MessageQueue{rdb: rdb}
}

// Отправка сообщения в поток
func (mq *MessageQueue) Publish(ctx context.Context, stream string, values map[string]interface{}) (string, error) {
return mq.rdb.XAdd(ctx, &redis.XAddArgs{
Stream: stream,
Values: values,
}).Result()
}

// Чтение сообщений из потока
func (mq *MessageQueue) Consume(ctx context.Context, stream, group, consumer string, count int64) ([]redis.XMessage, error) {
streams, err := mq.rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: group,
Consumer: consumer,
Streams: []string{stream, ">"},
Count: count,
Block: 5 * time.Second,
}).Result()

if err != nil {
return nil, err
}

if len(streams) > 0 {
return streams[0].Messages, nil
}
return nil, nil
}

// Подтверждение обработки
func (mq *MessageQueue) Acknowledge(ctx context.Context, stream, group, messageID string) error {
return mq.rdb.XAck(ctx, stream, group, messageID).Err()
}

6. Геопространственные данные (Redis):

package main

import (
"context"
"fmt"

"github.com/redis/go-redis/v9"
)

type GeoService struct {
rdb *redis.Client
}

func NewGeoService() *GeoService {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
return &GeoService{rdb: rdb}
}

// Добавление локации
func (gs *GeoService) AddLocation(ctx context.Context, key string, name string, lon, lat float64) error {
return gs.rdb.GeoAdd(ctx, key, &redis.GeoLocation{
Name: name,
Longitude: lon,
Latitude: lat,
}).Err()
}

// Поиск ближайших
func (gs *GeoService) FindNearby(ctx context.Context, key string, lon, lat float64, radius float64, unit string) ([]redis.GeoLocation, error) {
return gs.rdb.GeoRadius(ctx, key, lon, lat, &redis.GeoRadiusQuery{
Radius: radius,
Unit: unit,
WithCoord: true,
WithDist: true,
Sort: "ASC",
}).Result()
}

7. Временные ряды (Cassandra, InfluxDB):

-- Cassandra для временных рядов
CREATE TABLE sensor_data (
sensor_id TEXT,
date DATE,
timestamp TIMESTAMP,
temperature DOUBLE,
humidity DOUBLE,
pressure DOUBLE,
PRIMARY KEY ((sensor_id, date), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC)
AND default_time_to_live = 2592000; -- 30 дней
// Запись метрик
func (s *CassandraSession) InsertMetric(ctx context.Context, sensorID string, temp, humidity, pressure float64) error {
now := time.Now()
date := now.Format("2006-01-02")

return s.session.Query(
`INSERT INTO sensor_data (sensor_id, date, timestamp, temperature, humidity, pressure)
VALUES (?, ?, ?, ?, ?, ?)`,
sensorID, date, now, temp, humidity, pressure,
).WithContext(ctx).Exec()
}

Сводная таблица:

ЗадачаЛучший выборПочему
КэшRedisБыстрый доступ в памяти, TTL
СессииRedisПростота, скорость, автоудаление
ЛогиMongoDB, ElasticsearchГибкая схема, TTL индексы
Реальное времяRedisPub/Sub, Streams, Sorted Sets
КаталогиMongoDBГибкая схема документов
ОчередиRedis Streams, KafkaНадёжность, consumer groups
ГеоданныеRedisGeo commands встроены
Временные рядыCassandra, InfluxDBОптимизация под запись, TTL
Графы отношенийNeo4jНативные графовые запросы

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

Вопрос 19. Что такое кэширование и для чего оно нужно? Какие данные не стоит кэшировать? Что такое негативный кэш?

Таймкод: 00:34:04

Ответ собеседника: Правильный. Кэширование — запоминание ответа при первом запросе и отдача сохранённого результата при последующих запросах без пересчёта. Используется для горячих запросов и статичных данных (справочники, тяжёлые запросы с JOIN). Не стоит кэшировать данные, которые часто обновляются: баланс счёта, количество товаров — иначе возможны некорректные состояния. Негативный кэш — кэширование ошибок, чтобы не спамить сервер повторными запросами при отсутствии данных (например, пользователь не найден — кэшируем ошибку на время).

Правильный ответ:

Определение кэширования:

Кэширование — это механизм временного хранения результатов вычислений или данных в быстром хранилище для ускорения последующих обращений к ним. Кэш позволяет избежать повторных дорогостоящих операций (запросы к БД, вычисления, сетевые вызовы).

Уровни кэширования:

┌─────────────────────────────────────────────┐
│ Клиент (Browser) │ ← HTTP Cache, LocalStorage
├─────────────────────────────────────────────┤
│ CDN │ ← Static assets, API responses
├─────────────────────────────────────────────┤
│ Обратный прокси │ ← Nginx, Varnish
├─────────────────────────────────────────────┤
│ Приложение │ ← In-memory cache
├─────────────────────────────────────────────┤
│ Распределённый кэш │ ← Redis, Memcached
├─────────────────────────────────────────────┤
│ База данных │ ← Query cache, Buffer pool
└─────────────────────────────────────────────┘

Реализация кэширования на Go:

1. In-memory кэш:

package main

import (
"sync"
"time"
)

type CacheItem struct {
Value interface{}
Expiration time.Time
}

type InMemoryCache struct {
items map[string]CacheItem
mu sync.RWMutex
ttl time.Duration
}

func NewInMemoryCache(ttl time.Duration) *Cache {
cache := &InMemoryCache{
items: make(map[string]CacheItem),
ttl: ttl,
}
go cache.cleanup()
return cache
}

func (c *InMemoryCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = CacheItem{
Value: value,
Expiration: time.Now().Add(c.ttl),
}
}

func (c *InMemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

item, found := c.items[key]
if !found {
return nil, false
}

if time.Now().After(item.Expiration) {
return nil, false
}

return item.Value, true
}

func (c *InMemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}

func (c *InMemoryCache) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
c.mu.Lock()
now := time.Now()
for key, item := range c.items {
if now.After(item.Expiration) {
delete(c.items, key)
}
}
c.mu.Unlock()
}
}

2. Redis кэш:

package main

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

type RedisCache struct {
client *redis.Client
prefix string
}

func NewRedisCache(addr, prefix string) *RedisCache {
client := redis.NewClient(&redis.Options{
Addr: addr,
})
return &RedisCache{
client: client,
prefix: prefix,
}
}

func (c *RedisCache) key(k string) string {
return fmt.Sprintf("%s:%s", c.prefix, k)
}

func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, c.key(key), data, ttl).Err()
}

func (c *RedisCache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, c.key(key)).Bytes()
if err == redis.Nil {
return fmt.Errorf("key not found")
}
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}

func (c *RedisCache) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, c.key(key)).Err()
}

// Cache-Aside паттерн
type UserService struct {
cache *RedisCache
db *sql.DB
}

func (s *UserService) GetUser(ctx context.Context, userID int) (*User, error) {
// 1. Проверяем кэш
var user User
err := s.cache.Get(ctx, fmt.Sprintf("user:%d", userID), &user)
if err == nil {
return &user, nil // Cache hit
}

// 2. Cache miss - загружаем из БД
user, err = s.loadUserFromDB(ctx, userID)
if err != nil {
return nil, err
}

// 3. Сохраняем в кэш
s.cache.Set(ctx, fmt.Sprintf("user:%d", userID), user, 1*time.Hour)

return &user, nil
}

Негативный кэш (Negative Caching):

package main

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

// NegativeCacheItem хранит информацию об отсутствии данных
type NegativeCacheItem struct {
Found bool `json:"found"`
Value interface{} `json:"value,omitempty"`
Error string `json:"error,omitempty"`
CachedAt time.Time `json:"cached_at"`
}

type NegativeCache struct {
client *redis.Client
positiveTTL time.Duration
negativeTTL time.Duration
}

func NewNegativeCache(client *redis.Client, positiveTTL, negativeTTL time.Duration) *NegativeCache {
return &NegativeCache{
client: client,
positiveTTL: positiveTTL,
negativeTTL: negativeTTL,
}
}

// GetOrLoad загружает данные с поддержкой негативного кэша
func (nc *NegativeCache) GetOrLoad(
ctx context.Context,
key string,
loader func() (interface{}, error),
) (interface{}, error) {
// Проверяем кэш
data, err := nc.client.Get(ctx, key).Bytes()
if err == nil {
var item NegativeCacheItem
if err := json.Unmarshal(data, &item); err == nil {
if item.Found {
return item.Value, nil
}
// Негативный кэш - возвращаем ошибку
return nil, fmt.Errorf(item.Error)
}
}

// Cache miss - загружаем данные
value, err := loader()

item := NegativeCacheItem{
CachedAt: time.Now(),
}

if err != nil {
// Кэшируем ошибку (негативный кэш)
item.Found = false
item.Error = err.Error()
itemData, _ := json.Marshal(item)
nc.client.Set(ctx, key, itemData, nc.negativeTTL)
return nil, err
}

// Кэшируем успешный результат
item.Found = true
item.Value = value
itemData, _ := json.Marshal(item)
nc.client.Set(ctx, key, itemData, nc.positiveTTL)

return value, nil
}

// Пример использования
type UserServiceWithNegativeCache struct {
cache *NegativeCache
db *sql.DB
}

func (s *UserServiceWithNegativeCache) GetUser(ctx context.Context, userID int) (*User, error) {
key := fmt.Sprintf("user:%d", userID)

result, err := s.cache.GetOrLoad(ctx, key, func() (interface{}, error) {
// Загрузка из БД
var user User
err := s.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", userID,
).Scan(&user.ID, &user.Name, &user.Email)

if err == sql.ErrNoRows {
return nil, fmt.Errorf("user not found")
}
if err != nil {
return nil, err
}
return &user, nil
})

if err != nil {
return nil, err
}

return result.(*User), nil
}

Данные, которые не стоит кэшировать:

// 1. Часто изменяющиеся данные
type BalanceService struct {
cache *RedisCache
}

// ПЛОХО: кэширование баланса
func (s *BalanceService) GetBalanceBad(userID int) (float64, error) {
// Баланс меняется при каждой транзакции - кэш быстро устаревает
key := fmt.Sprintf("balance:%d", userID)
var balance float64
if err := s.cache.Get(context.Background(), key, &balance); err == nil {
return balance, nil
}
// ...
}

// ХОРОШО: не кэшируем баланс, получаем из БД
func (s *BalanceService) GetBalanceGood(userID int) (float64, error) {
var balance float64
err := s.db.QueryRow("SELECT balance FROM accounts WHERE user_id = $1", userID).Scan(&balance)
return balance, err
}

// 2. Чувствительные данные
type AuthService struct {
// ПЛОХО: кэширование паролей, токенов, персональных данных
}

// 3. Уникальные данные
type AnalyticsService struct {
// ПЛОХО: кэширование уникальных событий аналитики
// Каждое событие уникально - кэш никогда не используется
}

Паттерны инвалидации кэша:

// 1. TTL (Time-To-Live)
func (c *RedisCache) SetWithTTL(key string, value interface{}, ttl time.Duration) {
c.client.Set(context.Background(), key, value, ttl)
}

// 2. Явная инвалидация при обновлении
func (s *UserService) UpdateUser(ctx context.Context, user *User) error {
// Обновляем в БД
_, err := s.db.ExecContext(ctx,
"UPDATE users SET name = $1, email = $2 WHERE id = $3",
user.Name, user.Email, user.ID,
)
if err != nil {
return err
}

// Инвалидируем кэш
s.cache.Delete(ctx, fmt.Sprintf("user:%d", user.ID))

return nil
}

// 3. Cache-Aside с инвалидацией
func (s *UserService) GetUserWithInvalidation(ctx context.Context, userID int) (*User, error) {
key := fmt.Sprintf("user:%d", userID)

// Проверяем кэш
var user User
if err := s.cache.Get(ctx, key, &user); err == nil {
return &user, nil
}

// Загружаем из БД
user, err := s.loadFromDB(ctx, userID)
if err != nil {
return nil, err
}

// Сохраняем в кэш
s.cache.Set(ctx, key, user, 1*time.Hour)

return &user, nil
}

// 4. Write-Through
func (s *UserService) UpdateUserWriteThrough(ctx context.Context, user *User) error {
// Обновляем БД и кэш одновременно
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

_, err = tx.ExecContext(ctx,
"UPDATE users SET name = $1 WHERE id = $2",
user.Name, user.ID,
)
if err != nil {
return err
}

if err := tx.Commit(); err != nil {
return err
}

// Обновляем кэш
key := fmt.Sprintf("user:%d", user.ID)
s.cache.Set(ctx, key, user, 1*time.Hour)

return nil
}

Проблемы кэширования:

// 1. Cache Stampede (Лавина)
// Решение: Singleflight
type SafeCache struct {
cache *RedisCache
sf singleflight.Group
}

func (sc *SafeCache) GetOrLoad(ctx context.Context, key string, loader func() (interface{}, error)) (interface{}, error) {
// Проверяем кэш
var result interface{}
if err := sc.cache.Get(ctx, key, &result); err == nil {
return result, nil
}

// Singleflight предотвращает множественные загрузки
v, err, _ := sc.sf.Do(key, func() (interface{}, error) {
return loader()
})

if err != nil {
return nil, err
}

// Сохраняем в кэш
sc.cache.Set(ctx, key, v, 1*time.Hour)
return v, nil
}

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

Вопрос 20. Чем отличаются протоколы TCP и UDP?

Таймкод: 00:37:07

Ответ собеседника: Правильный. TCP — транспортный протокол с гарантией доставки, устанавливает соединение, имеет чёткий порядок пакетов и ретрай (повторная отправка). Используется для загрузки файлов, работы с базами данных, HTTPS. UDP не гарантирует ни доставку, ни порядок, но работает быстрее и расходует меньше ресурсов. Используется для стриминга, онлайн-игр, где важна скорость, а не гарантированная доставка каждого пакета.

Правильный ответ:

Определения:

TCP (Transmission Control Protocol) — протокол с установлением соединения, гарантирующий надёжную доставку данных в правильном порядке.

UDP (User Datagram Protocol) — протокол без установления соединения, не гарантирующий доставку и порядок пакетов.

Сравнительная таблица:

ХарактеристикаTCPUDP
СоединениеС установлением (3-way handshake)Без установления
НадёжностьГарантированаНе гарантирована
ПорядокГарантированНе гарантирован
СкоростьМедленнееБыстрее
Размер заголовка20-60 байт8 байт
Контроль перегрузокДаНет
ПрименениеHTTP, HTTPS, FTP, SSHDNS, VoIP, стриминг

TCP Three-Way Handshake:

Клиент Сервер
│ │
│──── SYN (seq=x) ────────▶│
│ │
│◀── SYN-ACK (seq=y, ─────│
│ ack=x+1) │
│ │
│──── ACK (ack=y+1) ──────▶│
│ │
│ Соединение установлено │

Реализация на Go:

TCP сервер:

package main

import (
"bufio"
"fmt"
"net"
"strings"
)

func main() {
// Создаём TCP listener
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()

fmt.Println("TCP server listening on :8080")

for {
// Принимаем соединение
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept error:", err)
continue
}

// Обрабатываем в отдельной горутине
go handleConnection(conn)
}
}

func handleConnection(conn net.Conn) {
defer conn.Close()

reader := bufio.NewReader(conn)

for {
// Читаем данные
message, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Client disconnected")
return
}

message = strings.TrimSpace(message)
fmt.Printf("Received: %s\n", message)

// Отправляем ответ
response := fmt.Sprintf("Echo: %s\n", message)
conn.Write([]byte(response))
}
}

TCP клиент:

package main

import (
"bufio"
"fmt"
"net"
"os"
)

func main() {
// Подключаемся к серверу
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
panic(err)
}
defer conn.Close()

// Отправляем сообщения
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
message := scanner.Text()

// Отправляем
fmt.Fprintf(conn, "%s\n", message)

// Читаем ответ
response, _ := bufio.NewReader(conn).ReadString('\n')
fmt.Print("Response:", response)
}
}

UDP сервер:

package main

import (
"fmt"
"net"
)

func main() {
// Создаём UDP listener
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
panic(err)
}

conn, err := net.ListenUDP("udp", addr)
if err != nil {
panic(err)
}
defer conn.Close()

fmt.Println("UDP server listening on :8080")

buffer := make([]byte, 1024)

for {
// Читаем датаграмму
n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Read error:", err)
continue
}

message := string(buffer[:n])
fmt.Printf("Received from %s: %s\n", clientAddr, message)

// Отправляем ответ
response := fmt.Sprintf("Echo: %s", message)
conn.WriteToUDP([]byte(response), clientAddr)
}
}

UDP клиент:

package main

import (
"fmt"
"net"
"os"
)

func main() {
// Разрешаем адрес сервера
serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8080")
if err != nil {
panic(err)
}

// Создаём UDP соединение
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
panic(err)
}
defer conn.Close()

// Отправляем сообщения
message := "Hello UDP"
conn.Write([]byte(message))

// Читаем ответ
buffer := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Read error:", err)
return
}

fmt.Printf("Response: %s\n", string(buffer[:n]))
}

Примеры использования:

// TCP для HTTP сервера
func startHTTPServer() {
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// Надёжная доставка данных
json.NewEncoder(w).Encode(users)
})
http.ListenAndServe(":8080", nil)
}

// UDP для DNS резолвера
func dnsResolver() {
conn, _ := net.Dial("udp", "8.8.8.8:53")
defer conn.Close()

// Отправляем DNS запрос
conn.Write(dnsQuery)

// Получаем ответ
buffer := make([]byte, 512)
n, _ := conn.Read(buffer)
parseDNSResponse(buffer[:n])
}

// UDP для стриминга
type VideoStreamer struct {
conn *net.UDPConn
}

func (vs *VideoStreamer) StreamFrame(frame []byte) error {
// Отправляем кадр без гарантии доставки
// Потеря одного кадра не критична для видео
_, err := vs.conn.Write(frame)
return err
}

Когда использовать TCP:

✓ Веб-серверы (HTTP/HTTPS)
✓ Базы данных (PostgreSQL, MySQL)
✓ Файловые передачи (FTP, SFTP)
✓ Email (SMTP, IMAP)
✓ Удалённый доступ (SSH)
✓ Любые данные, где важна целостность

Когда использовать UDP:

✓ DNS запросы
✓ Видео/аудио стриминг
✓ Онлайн игры
✓ VoIP (голосовая связь)
✓ IoT датчики
✓ Мультикаст рассылки

QUIC и HTTP/3:

// QUIC — протокол на основе UDP с надёжностью TCP
// Используется в HTTP/3

import (
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
)

func startHTTP3Server() {
server := &http3.Server{
Addr: ":443",
TLSConfig: tlsConfig,
Handler: handler,
}
server.ListenAndServe()
}

Вывод: TCP обеспечивает надёжную доставку с гарантией порядка, UDP — быструю передачу без гарантий. Выбор зависит от требований приложения: целостность данных vs скорость.

Вопрос 21. Что такое DNS?

Таймкод: 00:38:27

Ответ собеседника: Правильный. DNS (Domain Name System) — система домённых имён, которая сопоставляет IP-адреса с доменными именами. Все сайты имеют IP-адреса, но пользователям удобнее запоминать имена (например, google.com). DNS по доменному имени находит соответствующий IP и направляет клиента на нужный сервер. Аналогия — телефонная книга.

Правильный ответ:

Определение:

DNS (Domain Name System) — распределённая иерархическая система для преобразования человекочитаемых доменных имён в IP-адреса и обратно. Работает как «телефонная книга интернета».

Иерархия DNS:

┌─────────┐
│ . │ ← Root DNS
└────┬────┘

┌───────────────┼───────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ .com │ │ .org │ │ .net │ ← TLD
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ google │ │ wikipedia│ │ example │ ← Second Level
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ www │ │ en │ │ api │ ← Subdomain
└─────────┘ └─────────┘ └─────────┘

Типы DNS-записей:

ТипОписаниеПример
AIPv4 адресexample.com → 93.184.216.34
AAAAIPv6 адресexample.com → 2606:2800:220:1:248:1893:25c8:1946
CNAMEПсевдоним (alias)www.example.com → example.com
MXПочтовый серверexample.com → mail.example.com
NSDNS-сервер доменаexample.com → ns1.example.com
TXTТекстовая информацияSPF, DKIM записи
SOAНачало зоныМетаданные домена

Процесс DNS-резолвинга:

Пользователь Локальный DNS Корневой DNS
│ │ │
│── www.example.com ───────▶│ │
│ │ │
│ │── Запрос к корню ──────▶│
│ │◀── NS для .com ────────│
│ │ │
│ │── Запрос к .com ──────▶│
│ │◀── NS для example.com ─│
│ │ │
│ │── Запрос к example ───▶│
│ │◀── A 93.184.216.34 ────│
│ │ │
│◀── 93.184.216.34 ────────│ │

Реализация DNS-резолвера на Go:

package main

import (
"context"
"fmt"
"net"
"time"
)

func main() {
// Простой резолвинг
ips, err := net.LookupIP("google.com")
if err != nil {
panic(err)
}
for _, ip := range ips {
fmt.Println("IP:", ip)
}

// Резолвинг с контекстом
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 5 * time.Second,
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}

ips, err = resolver.LookupIP(ctx, "ip", "example.com")
if err != nil {
panic(err)
}
for _, ip := range ips {
fmt.Println("IP:", ip)
}
}

DNS-сервер на Go:

package main

import (
"fmt"
"log"
"net"

"github.com/miekg/dns"
)

func main() {
// Регистрируем обработчик
dns.HandleFunc(".", handleDNSRequest)

// Запускаем сервер
server := &dns.Server{Addr: ":53", Net: "udp"}
log.Println("Starting DNS server on :53")

err := server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}

func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
msg := new(dns.Msg)
msg.SetReply(r)

for _, q := range r.Question {
switch q.Qtype {
case dns.TypeA:
// Возвращаем фиксированный IP для всех запросов
rr, err := dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", q.Name))
if err == nil {
msg.Answer = append(msg.Answer, rr)
}
}
}

w.WriteMsg(msg)
}

DNS-кэширование:

package main

import (
"sync"
"time"
)

type DNSCache struct {
records map[string]CacheEntry
mu sync.RWMutex
}

type CacheEntry struct {
IPs []string
ExpiresAt time.Time
}

func NewDNSCache() *DNSCache {
cache := &DNSCache{
records: make(map[string]CacheEntry),
}
go cache.cleanup()
return cache
}

func (c *DNSCache) Get(domain string) ([]string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

entry, found := c.records[domain]
if !found || time.Now().After(entry.ExpiresAt) {
return nil, false
}
return entry.IPs, true
}

func (c *DNSCache) Set(domain string, ips []string, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()

c.records[domain] = CacheEntry{
IPs: ips,
ExpiresAt: time.Now().Add(ttl),
}
}

func (c *DNSCache) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
c.mu.Lock()
now := time.Now()
for domain, entry := range c.records {
if now.After(entry.ExpiresAt) {
delete(c.records, domain)
}
}
c.mu.Unlock()
}
}

// CachedResolver с кэшированием
type CachedResolver struct {
cache *DNSCache
resolver *net.Resolver
}

func (r *CachedResolver) Resolve(domain string) ([]string, error) {
// Проверяем кэш
if ips, found := r.cache.Get(domain); found {
return ips, nil
}

// Резолвим
ips, err := r.resolver.LookupIP(context.Background(), "ip", domain)
if err != nil {
return nil, err
}

// Сохраняем в кэш
var ipStrings []string
for _, ip := range ips {
ipStrings = append(ipStrings, ip.String())
}
r.cache.Set(domain, ipStrings, 5*time.Minute)

return ipStrings, nil
}

DNS Load Balancing:

// Round-robin DNS
type RoundRobinDNS struct {
records []string
index int
mu sync.Mutex
}

func NewRoundRobinDNS(records []string) *RoundRobinDNS {
return &RoundRobinDNS{records: records}
}

func (rr *RoundRobinDNS) Next() string {
rr.mu.Lock()
defer rr.mu.Unlock()

record := rr.records[rr.index]
rr.index = (rr.index + 1) % len(rr.records)
return record
}

// Использование
func main() {
dns := NewRoundRobinDNS([]string{
"192.168.1.1",
"192.168.1.2",
"192.168.1.3",
})

// Каждый следующий запрос получит другой IP
for i := 0; i < 6; i++ {
fmt.Println(dns.Next())
}
}

DNS over HTTPS (DoH):

package main

import (
"context"
"fmt"

"github.com/miekg/dns"
)

func dohResolve(domain string) error {
// Создаём DNS клиент через HTTPS
client := &dns.Client{
Net: "https",
}

// Создаём запрос
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), dns.TypeA)

// Отправляем через Cloudflare DoH
resp, _, err := client.Exchange(msg, "https://cloudflare-dns.com/dns-query")
if err != nil {
return err
}

// Обрабатываем ответ
for _, ans := range resp.Answer {
if a, ok := ans.(*dns.A); ok {
fmt.Printf("%s -> %s\n", domain, a.A.String())
}
}

return nil
}

Вывод: DNS — критически важная инфраструктура интернета, обеспечивающая преобразование доменных имён в IP-адреса. Понимание DNS необходимо для отладки сетевых проблем, настройки балансировки нагрузки и обеспечения безопасности.

Вопрос 22. Что такое атака Man in the Middle (человек посередине)?

Таймкод: 00:39:30

Ответ собеседника: Правильный. Man in the Middle — атака, при которой злоумышленник встраивается между клиентом и сервером, перехватывая и потенциально подменяя данные. Может красть чувствительные данные пользователя или перенаправлять трафик. Возможна при небезопасной сети. Защита — HTTPS протокол с шифрованием.

Правильный ответ:

Определение:

Man in the Middle (MITM) — атака, при которой злоумышленник тайно перехватывает и потенциально изменяет коммуникацию между двумя сторонами, которые считают, что общаются напрямую друг с другом.

Схема атаки:

Нормальная коммуникация:
┌────────┐ ┌────────┐
│ Клиент │◄──────────────────▶│ Сервер │
└────────┘ └────────┘

MITM атака:
┌────────┐ ┌──────────────┐ ┌────────┐
│ Клиент │◄────▶│ Злоумышленник │◄────▶│ Сервер │
└────────┘ └──────────────┘ └────────┘


Перехват данных
Подмена сообщений
Кража учётных данных

Типы MITM атак:

1. ARP Spoofing (подмена ARP):

Легитимный ARP:
Клиент (192.168.1.10) → MAC: AA:BB:CC:DD:EE:FF

Подменённый ARP:
Клиент (192.168.1.10) → MAC: 11:22:33:44:55:66 (злоумышленник)

2. DNS Spoofing (подмена DNS):

Запрос: google.com → DNS сервер
Легитимный ответ: google.com → 142.250.185.206
Подменённый ответ: google.com → 192.168.1.100 (злоумышленник)

3. SSL Stripping (понижение протокола):

Клиент ──HTTP──▶ Злоумышленник ──HTTPS──▶ Сервер
▲ │
└─────────── Перехват ────────────────┘

Реализация защиты на Go:

1. HTTPS сервер с TLS:

package main

import (
"crypto/tls"
"log"
"net/http"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)

// Настройка TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
},
PreferServerCipherSuites: true,
}

server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: tlsConfig,
}

log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}

2. Certificate Pinning:

package main

import (
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"fmt"
"net/http"
)

// Ожидаемый отпечаток сертификата
const expectedFingerprint = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

func createPinnedClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
for _, cert := range rawCerts {
fingerprint := sha256.Sum256(cert)
encoded := base64.StdEncoding.EncodeToString(fingerprint[:])
expected := "sha256/" + encoded

if expected != expectedFingerprint {
return fmt.Errorf("certificate fingerprint mismatch: got %s, want %s",
expected, expectedFingerprint)
}
}
return nil
},
},
},
}
}

3. HSTS (HTTP Strict Transport Security):

func hstsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Принудительное использование HTTPS
w.Header().Set("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload")
next.ServeHTTP(w, r)
})
}

4. Mutual TLS (mTLS):

package main

import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
)

func createMTLSClient() (*http.Client, error) {
// Загружаем клиентский сертификат
cert, err := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")
if err != nil {
return nil, err
}

// Загружаем CA сертификат
caCert, err := ioutil.ReadFile("ca-cert.pem")
if err != nil {
return nil, err
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}

return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}, nil
}

Обнаружение MITM:

package main

import (
"crypto/tls"
"fmt"
"net/http"
)

func checkTLSConnection(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.TLS == nil {
return fmt.Errorf("connection is not encrypted")
}

fmt.Printf("TLS Version: %s\n", tlsVersionName(resp.TLS.Version))
fmt.Printf("Cipher Suite: %s\n", cipherSuiteName(resp.TLS.CipherSuite))

for _, cert := range resp.TLS.PeerCertificates {
fmt.Printf("Subject: %s\n", cert.Subject)
fmt.Printf("Issuer: %s\n", cert.Issuer)
fmt.Printf("Expires: %s\n", cert.NotAfter)
}

return nil
}

func tlsVersionName(version uint16) string {
switch version {
case tls.VersionTLS10:
return "TLS 1.0"
case tls.VersionTLS11:
return "TLS 1.1"
case tls.VersionTLS12:
return "TLS 1.2"
case tls.VersionTLS13:
return "TLS 1.3"
default:
return "Unknown"
}
}

Защита от ARP Spoofing:

# Статические ARP записи
arp -s 192.168.1.1 AA:BB:CC:DD:EE:FF

# Мониторинг ARP таблицы
arpwatch

Вывод: MITM атаки перехватывают коммуникацию между клиентом и сервером. Основные методы защиты: HTTPS/TLS, certificate pinning, HSTS, mutual TLS. Важно всегда использовать шифрование и проверять сертификаты.

Вопрос 23. Что такое центры сертификации и зачем они нужны?

Таймкод: 00:40:36

Ответ собеседника: Правильный. Центры сертификации — организации, выпускающие цифровые сертификаты для доменов/серверов. Сертификат подтверждает, что сайт проверен и ему можно доверять. Используется в HTTPS: браузер проверяет сертификат и предупреждает пользователя, если сайт небезопасен. Центр валидирует домен и подтверждает его принадлежность.

Правильный ответ:

Определение:

Центр сертификации (Certificate Authority, CA) — доверенная организация, которая выпускает, управляет и проверяет цифровые сертификаты, используемые для подтверждения подлинности веб-сайтов, серверов и других сущностей в интернете.

Иерархия доверия:

┌─────────────────┐
│ Root CA │ ← Самоподписанный сертификат
│ (DigiCert, │
│ Let's Encrypt)│
└────────┬────────┘

┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌────┴────┐ ┌─────┴─────┐
│Intermediate│ │Intermediate│ │Intermediate│
│ CA 1 │ │ CA 2 │ │ CA 3 │
└─────┬─────┘ └────┬────┘ └─────┬─────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ End- │ │ End- │ │ End- │
│ Entity │ │ Entity │ │ Entity │
│ Cert │ │ Cert │ │ Cert │
└─────────┘ └─────────┘ └─────────┘

Типы сертификатов:

ТипВалидацияПримерВремя выпуска
DV (Domain Validation)Подтверждение владения доменомLet's EncryptМинуты
OV (Organization Validation)Проверка организацииComodo, DigiCert1-3 дня
EV (Extended Validation)Расширенная проверкаDigiCert EV1-2 недели
WildcardВсе поддомены одного домена*.example.comЗависит от типа

Процесс выпуска сертификата:

1. Генерация ключей
┌──────────┐
│ Сервер │ → Создаёт пару ключей (public + private)
└──────────┘

2. Создание CSR (Certificate Signing Request)
┌──────────┐
│ Сервер │ → Отправляет публичный ключ + информацию о домене
└──────────┘


3. Валидация CA
┌──────────┐
│ CA │ → Проверяет владение доменом (DNS, HTTP, Email)
└──────────┘


4. Подписание сертификата
┌──────────┐
│ CA │ → Подписывает публичный ключ своим приватным ключом
└──────────┘


5. Установка сертификата
┌──────────┐
│ Сервер │ → Устанавливает полученный сертификат
└──────────┘

Реализация на Go:

1. Генерация самоподписанного сертификата:

package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"time"
)

func generateSelfSignedCert() error {
// Генерируем приватный ключ
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}

// Создаём шаблон сертификата
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"My Company"},
CommonName: "localhost",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"localhost", "*.example.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
BasicConstraintsValid: true,
}

// Создаём самоподписанный сертификат
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return err
}

// Сохраняем сертификат
certFile, err := os.Create("cert.pem")
if err != nil {
return err
}
defer certFile.Close()

pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})

// Сохраняем приватный ключ
keyDER, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return err
}

keyFile, err := os.Create("key.pem")
if err != nil {
return err
}
defer keyFile.Close()

pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})

return nil
}

2. Загрузка и использование сертификата:

package main

import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
)

func loadTLSCertificate(certFile, keyFile string) (tls.Certificate, error) {
return tls.LoadX509KeyPair(certFile, keyFile)
}

func createServerWithTLS(cert tls.Certificate) *http.Server {
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}

return &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
}
}

3. Клиент с проверкой сертификата:

package main

import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
)

func createSecureClient(caCertFile string) (*http.Client, error) {
// Загружаем CA сертификат
caCert, err := ioutil.ReadFile(caCertFile)
if err != nil {
return nil, err
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Создаём клиент с проверкой сертификата
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
},
},
}

return client, nil
}

4. Let's Encrypt с использованием autocert:

package main

import (
"crypto/tls"
"log"
"net/http"

"golang.org/x/crypto/acme/autocert"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, HTTPS!"))
})

// Автоматическое получение сертификата от Let's Encrypt
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"),
Cache: autocert.DirCache("certs"),
}

server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: certManager.TLSConfig(),
}

// HTTP сервер для ACME challenge и редиректа на HTTPS
go http.ListenAndServe(":80", certManager.HTTPHandler(nil))

log.Fatal(server.ListenAndServeTLS("", ""))
}

Проверка сертификата:

package main

import (
"crypto/tls"
"fmt"
"net"
"time"
)

func checkCertificate(domain string) error {
conn, err := tls.DialWithDialer(&net.Dialer{
Timeout: 5 * time.Second,
}, "tcp", domain+":443", &tls.Config{
InsecureSkipVerify: false,
})
if err != nil {
return err
}
defer conn.Close()

certs := conn.ConnectionState().PeerCertificates
for _, cert := range certs {
fmt.Printf("Subject: %s\n", cert.Subject)
fmt.Printf("Issuer: %s\n", cert.Issuer)
fmt.Printf("Valid From: %s\n", cert.NotBefore)
fmt.Printf("Valid Until: %s\n", cert.NotAfter)
fmt.Printf("DNS Names: %v\n", cert.DNSNames)
}

return nil
}

CRL и OCSP (отзыв сертификатов):

// CRL (Certificate Revocation List)
// Список отозванных сертификатов

// OCSP (Online Certificate Status Protocol)
// Онлайн проверка статуса сертификата

func checkOCSP(cert *x509.Certificate) error {
if len(cert.OCSPServer) == 0 {
return fmt.Errorf("no OCSP server in certificate")
}

// Запрос к OCSP серверу
ocspURL := cert.OCSPServer[0]
// ... логика проверки
return nil
}

Известные центры сертификации:

ТипCAОсобенности
БесплатныйLet's EncryptАвтоматизация, DV сертификаты
КоммерческийDigiCertEV, OV, поддержка
КоммерческийSectigo (Comodo)Различные типы
КоммернийGlobalSignEnterprise решения

Вывод: Центры сертификации обеспечивают доверие в интернете через выпуск и проверку цифровых сертификатов. Они подтверждают подлинность сайтов и защищают от MITM атак через шифрование HTTPS.

Вопрос 24. Что такое атака CSRF (Cross-Site Request Forgery)?

Таймкод: 00:42:00

Ответ собеседника: Правильный. CSRF — атака с подменой межсайтового запроса. Авторизованный пользователь открывает вредоносную страницу, которая отправляет запрос от его имени (используя куки браузера). Сервер воспринимает запрос как валидный. Пример: пользователь залогинен в банке, открывает вредоносную страницу, которая отправляет запрос на перевод денег. Защита — CSRF-токены и проверка заголовков (origin/referer).

Правильный ответ:

Определение:

CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер авторизованного пользователя выполнить нежелательное действие на доверенном сайте, используя автоматическую отправку кук и сессионных данных.

Схема атаки:

1. Пользователь авторизуется на bank.com
┌──────────┐ ┌──────────┐
│ Пользователь │────▶│ bank.com │
└──────────┘ └──────────┘
│ │
│◀──── Cookie ────────│
│ session_id │

2. Пользователь посещает вредоносный сайт
┌──────────┐ ┌──────────────┐
│ Пользователь │────▶│ evil.com │
└──────────┘ └──────────────┘
│ │
│◀──── HTML с формой ─│
│ для bank.com │

3. Браузер автоматически отправляет запрос с куками
┌──────────┐ ┌──────────┐
│ Пользователь │────▶│ bank.com │
└──────────┘ Cookie └──────────┘
session_id


Перевод денег на счёт
злоумышленника

Пример вредоносного кода:

<!-- Страница на evil.com -->
<html>
<body>
<h1>Поздравляем! Вы выиграли!</h1>

<!-- Скрытая форма, которая отправится автоматически -->
<form id="csrf-form" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>

<script>
// Автоматическая отправка формы
document.getElementById('csrf-form').submit();
</script>
</body>
</html>

Реализация защиты на Go:

1. CSRF-токены:

package main

import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"html/template"
"net/http"
)

type CSRFProtection struct {
secret []byte
}

func NewCSRFProtection() *CSRFProtection {
secret := make([]byte, 32)
rand.Read(secret)
return &CSRFProtection{secret: secret}
}

// GenerateToken создаёт CSRF-токен
func (c *CSRFProtection) GenerateToken(sessionID string) string {
// В реальном приложении используйте HMAC
token := make([]byte, 32)
rand.Read(token)
return base64.StdEncoding.EncodeToString(token)
}

// ValidateToken проверяет CSRF-токен
func (c *CSRFProtection) ValidateToken(r *http.Request) bool {
// Получаем токен из формы
formToken := r.FormValue("csrf_token")

// Получаем токен из куки или сессии
cookieToken, err := r.Cookie("csrf_token")
if err != nil {
return false
}

// Константное сравнение для предотвращения timing-атак
return subtle.ConstantTimeCompare(
[]byte(formToken),
[]byte(cookieToken.Value),
) == 1
}

// Middleware для проверки CSRF
func (c *CSRFProtection) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Проверяем только изменяющие методы
if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
if !c.ValidateToken(r) {
http.Error(w, "CSRF validation failed", http.StatusForbidden)
return
}
}

next.ServeHTTP(w, r)
})
}

// Пример использования
func main() {
csrf := NewCSRFProtection()

mux := http.NewServeMux()

// Страница с формой
mux.HandleFunc("/transfer", func(w http.ResponseWriter, r *http.Request) {
token := csrf.GenerateToken("session_id")

// Устанавливаем токен в куку
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
HttpOnly: false, // Должен быть доступен из JavaScript
Secure: true,
SameSite: http.SameSiteStrictMode,
})

// Рендерим форму с токеном
tmpl := `
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{.Token}}">
<input type="text" name="to" placeholder="Получатель">
<input type="number" name="amount" placeholder="Сумма">
<button type="submit">Перевести</button>
</form>
`
t := template.Must(template.New("form").Parse(tmpl))
t.Execute(w, map[string]string{"Token": token})
})

// Обработчик формы
mux.HandleFunc("/transfer", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
to := r.FormValue("to")
amount := r.FormValue("amount")
// Обработка перевода
w.Write([]byte("Transfer completed"))
}
})

// Применяем CSRF защиту
handler := csrf.Middleware(mux)

http.ListenAndServe(":8080", handler)
}

2. SameSite Cookie атрибут:

func setSecureCookie(w http.ResponseWriter, name, value string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
HttpOnly: true, // Недоступен из JavaScript
Secure: true, // Только через HTTPS
SameSite: http.SameSiteStrictMode, // Не отправлять с других сайтов
Path: "/",
MaxAge: 3600,
})
}

// SameSite значения:
// - Strict: кука НЕ отправляется при переходе с другого сайта
// - Lax: кука отправляется только при GET запросах с другого сайта
// - None: кука всегда отправляется (требует Secure)

3. Проверка Origin и Referer заголовков:

func checkOriginMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")

allowed := false

// Проверяем Origin
for _, allowedOrigin := range allowedOrigins {
if origin == allowedOrigin {
allowed = true
break
}
}

// Если Origin отсутствует, проверяем Referer
if !allowed && origin == "" {
for _, allowedOrigin := range allowedOrigins {
if strings.HasPrefix(referer, allowedOrigin) {
allowed = true
break
}
}
}

if !allowed && (origin != "" || referer != "") {
http.Error(w, "Origin not allowed", http.StatusForbidden)
return
}

next.ServeHTTP(w, r)
})
}
}

4. Double Submit Cookie паттерн:

// Клиентская часть (JavaScript)
/*
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token') // Добавляем токен в заголовок
},
body: JSON.stringify({to: 'account', amount: 100})
});
*/

// Серверная часть
func doubleSubmitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
// Токен из заголовка
headerToken := r.Header.Get("X-CSRF-Token")

// Токен из куки
cookie, err := r.Cookie("csrf_token")
if err != nil {
http.Error(w, "CSRF cookie missing", http.StatusForbidden)
return
}

// Сравниваем
if subtle.ConstantTimeCompare([]byte(headerToken), []byte(cookie.Value)) != 1 {
http.Error(w, "CSRF token mismatch", http.StatusForbidden)
return
}
}

next.ServeHTTP(w, r)
})
}

5. Полный пример с использованием gorilla/csrf:

package main

import (
"net/http"

"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)

func main() {
r := mux.NewRouter()

// CSRF защита
csrfMiddleware := csrf.Protect(
[]byte("32-byte-long-auth-key"),
csrf.Secure(true),
csrf.HttpOnly(true),
csrf.Path("/"),
csrf.MaxAge(3600),
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("CSRF validation failed"))
})),
)

// API маршруты (без CSRF для внешних API)
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/login", loginHandler).Methods("POST")

// Веб маршруты (с CSRF защитой)
web := r.PathPrefix("").Subrouter()
web.Use(csrfMiddleware)
web.HandleFunc("/transfer", transferPageHandler).Methods("GET")
web.HandleFunc("/transfer", transferHandler).Methods("POST")

http.ListenAndServe(":8080", r)
}

func transferPageHandler(w http.ResponseWriter, r *http.Request) {
// В шаблоне используем csrf.TemplateField
tmpl := `
<form method="POST">
{{ .csrfField }}
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Transfer</button>
</form>
`
t := template.Must(template.New("form").Parse(tmpl))
t.Execute(w, map[string]interface{}{
"csrfField": csrf.TemplateField(r),
})
}

Сравнение методов защиты:

МетодПлюсыМинусы
CSRF-токеныНадёжная защитаСложнее реализация
SameSite CookieПростотаНе все браузеры поддерживают
Origin/RefererПростоМожет быть отключено пользователем
Double SubmitДля SPAТребует JavaScript

Вывод: CSRF атаки используют автоматическую отправку кук браузером. Основные методы защиты: CSRF-токены, SameSite атрибут кук, проверка Origin/Referer заголовков. Рекомендуется использовать комбинацию методов для максимальной защиты.

Вопрос 25. Что такое SQL-инъекция и как от неё защититься?

Таймкод: 00:43:45

Ответ собеседника: Правильный. SQL-инъекция — внедрение SQL-кода через формы ввода (регистрация, поиск и т.д.). Злоумышленник может выполнить произвольные SQL-команды, например DROP TABLE. Защита — использование параметризованных запросов (prepared statements) вместо конкатенации SQL-строк.

Правильный ответ:

Определение:

SQL-инъекция — атака, при которой злоумышленник внедряет вредоносный SQL-код в запросы к базе данных через пользовательский ввод, что позволяет выполнить произвольные SQL-команды.

Примеры уязвимого кода:

// УЯЗВИМЫЙ КОД - НЕ ДЕЛАЙТЕ ТАК!
func searchUsers(db *sql.DB, name string) ([]User, error) {
// Конкатенация строк - главная ошибка!
query := "SELECT * FROM users WHERE name = '" + name + "'"
rows, err := db.Query(query)
// ...
}

// Атака:
// name = "'; DROP TABLE users; --"
// Результат: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'

Типы SQL-инъекций:

1. Classic SQL Injection:

-- Оригинальный запрос
SELECT * FROM users WHERE username = 'admin' AND password = 'secret'

-- Инъекция
Username: admin' --
Password: anything

-- Результат
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'

2. Union-based Injection:

-- Инъекция
' UNION SELECT username, password, NULL FROM admin_users --

-- Результат
SELECT * FROM products WHERE name = ''
UNION SELECT username, password, NULL FROM admin_users --'

3. Blind SQL Injection:

-- Boolean-based
' AND 1=1 --
' AND 1=2 --

-- Time-based
'; WAITFOR DELAY '00:00:05' --
'; SELECT pg_sleep(5) --

Защита на Go:

1. Параметризованные запросы (Prepared Statements):

package main

import (
"database/sql"
"fmt"
)

type UserStore struct {
db *sql.DB
}

// ПРАВИЛЬНО: Используем параметризованные запросы
func (s *UserStore) GetUserByName(name string) (*User, error) {
// Плейсхолдеры $1, $2 и т.д. автоматически экранируются
query := "SELECT id, name, email FROM users WHERE name = $1"

var user User
err := s.db.QueryRow(query, name).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}

// ПРАВИЛЬНО: Вставка данных
func (s *UserStore) CreateUser(name, email string) error {
query := "INSERT INTO users (name, email) VALUES ($1, $2)"
_, err := s.db.Exec(query, name, email)
return err
}

// ПРАВИЛЬНО: Обновление данных
func (s *UserStore) UpdateUser(id int, name, email string) error {
query := "UPDATE users SET name = $1, email = $2 WHERE id = $3"
_, err := s.db.Exec(query, name, email, id)
return err
}

// ПРАВИЛЬНО: Удаление
func (s *UserStore) DeleteUser(id int) error {
query := "DELETE FROM users WHERE id = $1"
_, err := s.db.Exec(query, id)
return err
}

2. Использование ORM (GORM):

package main

import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
Email string `gorm:"size:100;uniqueIndex"`
}

type UserService struct {
db *gorm.DB
}

func NewUserService(dsn string) (*UserService, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}

// Автомиграция
db.AutoMigrate(&User{})

return &UserService{db: db}, nil
}

// GORM автоматически использует параметризованные запросы
func (s *UserService) GetUserByName(name string) (*User, error) {
var user User
result := s.db.Where("name = ?", name).First(&user)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}

func (s *UserService) CreateUser(name, email string) error {
user := User{Name: name, Email: email}
result := s.db.Create(&user)
return result.Error
}

func (s *UserService) SearchUsers(searchTerm string) ([]User, error) {
var users []User
// Безопасный LIKE запрос
result := s.db.Where("name ILIKE ?", "%"+searchTerm+"%").Find(&users)
return users, result.Error
}

3. Валидация и санитизация ввода:

package main

import (
"fmt"
"regexp"
"strings"
)

type InputValidator struct {
emailRegex *regexp.Regexp
}

func NewInputValidator() *InputValidator {
return &InputValidator{
emailRegex: regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
}
}

func (v *InputValidator) ValidateEmail(email string) error {
if !v.emailRegex.MatchString(email) {
return fmt.Errorf("invalid email format")
}
return nil
}

func (v *InputValidator) SanitizeString(input string) string {
// Удаляем потенциально опасные символы
dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_"}
result := input
for _, char := range dangerous {
result = strings.ReplaceAll(result, char, "")
}
return strings.TrimSpace(result)
}

func (v *InputValidator) ValidateUsername(username string) error {
if len(username) < 3 || len(username) > 50 {
return fmt.Errorf("username must be between 3 and 50 characters")
}

matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
if !matched {
return fmt.Errorf("username can only contain letters, numbers, and underscores")
}

return nil
}

4. Использование sqlx для безопасных запросов:

package main

import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)

type UserStore struct {
db *sqlx.DB
}

func NewUserStore(dsn string) (*UserStore, error) {
db, err := sqlx.Connect("postgres", dsn)
if err != nil {
return nil, err
}
return &UserStore{db: db}, nil
}

// Named queries - безопасны от SQL-инъекций
func (s *UserStore) CreateUser(user *User) error {
query := `
INSERT INTO users (name, email, age)
VALUES (:name, :email, :age)
`
_, err := s.db.NamedExec(query, user)
return err
}

// Select с параметрами
func (s *UserStore) GetUsersByAge(minAge, maxAge int) ([]User, error) {
query := `
SELECT id, name, email, age
FROM users
WHERE age BETWEEN $1 AND $2
`
var users []User
err := s.db.Select(&users, query, minAge, maxAge)
return users, err
}

5. Хранимые процедуры:

-- Создание хранимой процедуры
CREATE OR REPLACE FUNCTION get_user_by_email(p_email TEXT)
RETURNS TABLE(id INT, name TEXT, email TEXT) AS $$
BEGIN
RETURN QUERY
SELECT u.id, u.name, u.email
FROM users u
WHERE u.email = p_email;
END;
$$ LANGUAGE plpgsql;
func (s *UserStore) GetUserByEmail(email string) (*User, error) {
query := "SELECT * FROM get_user_by_email($1)"

var user User
err := s.db.QueryRow(query, email).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}

6. Принцип наименьших привилегий:

-- Создаём пользователя с минимальными привилегиями
CREATE USER app_user WITH PASSWORD 'secure_password';

-- Только SELECT, INSERT, UPDATE на нужные таблицы
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON products TO app_user;

-- Нет прав на DROP, ALTER, CREATE
-- Нет доступа к системным таблицам

7. Мониторинг и логирование:

package main

import (
"database/sql"
"log"
"time"
)

type AuditedDB struct {
db *sql.DB
}

func (adb *AuditedDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
start := time.Now()

// Логируем запрос
log.Printf("Query: %s, Args: %v", query, args)

rows, err := adb.db.Query(query, args...)

duration := time.Since(start)
log.Printf("Query took: %v, Error: %v", duration, err)

// Обнаружение подозрительных запросов
if isSuspiciousQuery(query) {
log.Printf("SUSPICIOUS QUERY DETECTED: %s", query)
}

return rows, err
}

func isSuspiciousQuery(query string) bool {
suspicious := []string{
"DROP TABLE",
"DROP DATABASE",
"UNION SELECT",
"INTO OUTFILE",
"LOAD_FILE",
"BENCHMARK",
"SLEEP(",
"PG_SLEEP",
}

upperQuery := strings.ToUpper(query)
for _, pattern := range suspicious {
if strings.Contains(upperQuery, pattern) {
return true
}
}
return false
}

Чек-лист защиты от SQL-инъекций:

✓ Всегда используйте параметризованные запросы
✓ Используйте ORM или query builder
✓ Валидируйте и санитизируйте ввод
✓ Применяйте принцип наименьших привилегий
✓ Используйте WAF (Web Application Firewall)
✓ Регулярно обновляйте СУБД
✓ Логируйте подозрительные запросы
✓ Проводите аудит безопасности
✗ Никогда не конкатенируйте пользовательский ввод в SQL
✗ Не показывайте детальные ошибки пользователю
✗ Не используйте root/超级user для приложения

Вывод: SQL-инъекция — одна из самых опасных уязвимостей. Основная защита — параметризованные запросы, которые автоматически экранируют пользовательский ввод. Дополнительно: валидация, принцип наименьших привилегий, мониторинг.

Вопрос 26. Что такое политики CORS (Cross-Origin Resource Sharing)?

Таймкод: 00:44:54

Ответ собеседника: Правильный. CORS — механизм контроля доступа к ресурсам с другого домена (origin). По умолчанию браузер блокирует запросы на другой origin. Сервер настраивает заголовки (Access-Control-Allow-Origin, Access-Control-Allow-Methods и т.д.), указывая разрешённые домены, методы и заголовки. Это позволяет безопасно разрешать кросс-доменные запросы.

Правильный ответ:

Определение:

CORS (Cross-Origin Resource Sharing) — механизм безопасности браузера, который контролирует доступ веб-приложений к ресурсам с другого источника (origin). Origin определяется комбинацией протокола, домена и порта.

Что считается другим origin:

Origin 1: https://example.com:443
Origin 2: https://api.example.com:443 ← Другой домен
Origin 3: http://example.com:443 ← Другой протокол
Origin 4: https://example.com:8080 ← Другой порт
Origin 5: https://evil.com:443 ← Другой домен

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

Простой запрос (Simple Request):
┌──────────┐ ┌──────────┐
│ Браузер │──── GET /api/users ────────▶│ Сервер │
│ │ │ │
│ │◀── Access-Control-Allow- ───│ │
│ │ Origin: https://app.com │ │
└──────────┘ └──────────┘

Preflight запрос (Non-Simple Request):
┌──────────┐ ┌──────────┐
│ Браузер │──── OPTIONS /api/users ────▶│ Сервер │
│ │ │ │
│ │◀── Access-Control-Allow- ───│ │
│ │ Origin: https://app.com │ │
│ │ Methods: POST, PUT │ │
│ │ Headers: Content-Type │ │
│ │ │ │
│ │──── POST /api/users ────────▶│ │
│ │ │ │
│ │◀── Response ────────────────│ │
└──────────┘ └──────────┘

CORS заголовки:

ЗаголовокНазначение
Access-Control-Allow-OriginРазрешённые origins
Access-Control-Allow-MethodsРазрешённые HTTP методы
Access-Control-Allow-HeadersРазрешённые заголовки
Access-Control-Allow-CredentialsРазрешены ли куки
Access-Control-Max-AgeВремя кэширования preflight
Access-Control-Expose-HeadersЗаголовки для чтения клиентом

Реализация на Go:

1. Базовый CORS middleware:

package main

import (
"net/http"
"strings"
)

type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
AllowCredentials bool
MaxAge int
}

func CORSMiddleware(config CORSConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")

// Проверяем, разрешён ли origin
if isOriginAllowed(origin, config.AllowedOrigins) {
w.Header().Set("Access-Control-Allow-Origin", origin)

if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
}

// Preflight запрос
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods",
strings.Join(config.AllowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers",
strings.Join(config.AllowedHeaders, ", "))
w.Header().Set("Access-Control-Max-Age",
strconv.Itoa(config.MaxAge))

w.WriteHeader(http.StatusNoContent)
return
}

next.ServeHTTP(w, r)
})
}
}

func isOriginAllowed(origin string, allowed []string) bool {
for _, o := range allowed {
if o == "*" || o == origin {
return true
}
}
return false
}

// Использование
func main() {
cors := CORSMiddleware(CORSConfig{
AllowedOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
AllowCredentials: true,
MaxAge: 86400,
})

mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)

handler := cors(mux)
http.ListenAndServe(":8080", handler)
}

2. Использование библиотеки rs/cors:

package main

import (
"net/http"

"github.com/rs/cors"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
mux.HandleFunc("/api/products", productsHandler)

// Настройка CORS
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Custom-Header"},
AllowCredentials: true,
MaxAge: 86400,
Debug: true, // Логировать CORS в консоль
})

// Оборачиваем handler
handler := c.Handler(mux)

http.ListenAndServe(":8080", handler)
}

3. CORS с валидацией origin:

package main

import (
"net/http"
"regexp"
)

type CORSHandler struct {
allowedOrigins []*regexp.Regexp
}

func NewCORSHandler() *CORSHandler {
return &CORSHandler{
allowedOrigins: []*regexp.Regexp{
regexp.MustCompile(`^https://.*\.example\.com$`),
regexp.MustCompile(`^https://app\.example\.com$`),
},
}
}

func (h *CORSHandler) isOriginAllowed(origin string) bool {
for _, pattern := range h.allowedOrigins {
if pattern.MatchString(origin) {
return true
}
}
return false
}

func (h *CORSHandler) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")

if origin != "" && h.isOriginAllowed(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
}

if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}

next.ServeHTTP(w, r)
})
}

4. CORS для API с аутентификацией:

package main

import (
"net/http"
)

func setupAPI() http.Handler {
mux := http.NewServeMux()

// Публичные эндпоинты
mux.HandleFunc("/api/health", healthHandler)
mux.HandleFunc("/api/login", loginHandler)

// Защищённые эндпойнты
authMux := http.NewServeMux()
authMux.HandleFunc("/api/users", usersHandler)
authMux.HandleFunc("/api/profile", profileHandler)

// Добавляем аутентификацию
authenticated := authMiddleware(authMux)
mux.Handle("/api/", authenticated)

// CORS с поддержкой credentials
cors := CORSMiddleware(CORSConfig{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true, // Важно для кук и авторизации
MaxAge: 3600,
})

return cors(mux)
}

5. Разные политики для разных маршрутов:

package main

import "net/http"

func main() {
// Публичный API — разрешаем все origins
publicCORS := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET"},
})

// Приватный API — только конкретные origins
privateCORS := cors.New(cors.Options{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
})

mux := http.NewServeMux()

// Публичные маршруты
mux.Handle("/api/public/", publicCORS.Handler(
http.StripPrefix("/api/public", publicHandler()),
))

// Приватные маршруты
mux.Handle("/api/private/", privateCORS.Handler(
http.StripPrefix("/api/private", privateHandler()),
))

http.ListenAndServe(":8080", mux)
}

Проблемы и решения:

// Проблема: несколько значений в Access-Control-Allow-Origin
// Нельзя: Access-Control-Allow-Origin: https://a.com, https://b.com

// Решение: проверяем origin и возвращаем конкретный
func handleCORS(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")

allowedOrigins := map[string]bool{
"https://app1.example.com": true,
"https://app2.example.com": true,
}

if allowedOrigins[origin] {
// Возвращаем конкретный origin, не "*"
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin") // Важно для кэширования
}
}

Вывод: CORS — важный механизм безопасности для контроля кросс-доменных запросов. Правильная настройка включает: ограничение origins, методов, заголовков, использование credentials. Для Go рекомендуется использовать библиотеку rs/cors или написать собственное middleware.

Вопрос 27. Как устроены интерфейсы в Go?

Таймкод: 00:46:03

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

Правильный ответ:

Определение:

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

Объявление и использование:

package main

import "fmt"

// Определение интерфейса
type Writer interface {
Write(p []byte) (n int, err error)
}

type Reader interface {
Read(p []byte) (n int, err error)
}

// Композиция интерфейсов
type ReadWriter interface {
Reader
Writer
}

// Конкретный тип, реализующий интерфейс
type File struct {
name string
}

// File реализует Writer
func (f *File) Write(p []byte) (n int, err error) {
fmt.Printf("Writing to %s: %s\n", f.name, string(p))
return len(p), nil
}

// File реализует Reader
func (f *File) Read(p []byte) (n int, err error) {
return 0, nil
}

// Функция, принимающая интерфейс
func WriteData(w Writer, data []byte) error {
_, err := w.Write(data)
return err
}

func main() {
file := &File{name: "test.txt"}

// File удовлетворяет Writer
WriteData(file, []byte("Hello"))

// Можно присвоить интерфейсной переменной
var w Writer = file
w.Write([]byte("World"))
}

Устройство интерфейса под капотом:

// Внутреннее представление интерфейса (упрощённо)
type iface struct {
tab *itab // Таблица методов (тип + методы)
data unsafe.Pointer // Указатель на данные
}

type itab struct {
inter *interfacetype // Тип интерфейса
_type *_type // Конкретный тип
hash uint32 // Хэш типа
_ [4]byte
fun [1]uintptr // Массив указателей на методы
}

// Пустой интерфейс (interface{})
type eface struct {
_type *_type // Тип данных
data unsafe.Pointer // Указатель на данные
}

Проверка типов и утверждения:

package main

import "fmt"

type Animal interface {
Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }

type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }

// Type assertion (утверждение типа)
func checkType(a Animal) {
// Безопасная проверка
if dog, ok := a.(Dog); ok {
fmt.Println("It's a dog:", dog.Speak())
}

// Switch по типу
switch v := a.(type) {
case Dog:
fmt.Println("Dog:", v.Speak())
case Cat:
fmt.Println("Cat:", v.Speak())
default:
fmt.Println("Unknown type")
}
}

func main() {
var a Animal = Dog{}
checkType(a)

a = Cat{}
checkType(a)
}

Пустой интерфейс interface{}:

package main

import "fmt"

// interface{} может содержать любой тип
func printValue(v interface{}) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Go 1.18+ используйте any вместо interface{}
func printValueModern(v any) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}

func main() {
printValue(42)
printValue("hello")
printValue(3.14)
printValue([]int{1, 2, 3})
}

Практические примеры:

1. Внедрение зависимостей:

package main

import "fmt"

// Интерфейс для работы с хранилищем
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}

type User struct {
ID int
Name string
}

// Реализация с PostgreSQL
type PostgresRepo struct {
db *sql.DB
}

func (r *PostgresRepo) FindByID(id int) (*User, error) {
// Запрос к PostgreSQL
return nil, nil
}

func (r *PostgresRepo) Save(user *User) error {
// Сохранение в PostgreSQL
return nil
}

// Реализация с MongoDB
type MongoRepo struct {
collection *mongo.Collection
}

func (r *MongoRepo) FindByID(id int) (*User, error) {
// Запрос к MongoDB
return nil, nil
}

func (r *MongoRepo) Save(user *User) error {
// Сохранение в MongoDB
return nil
}

// Сервис зависит от интерфейса, а не от конкретной реализации
type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}

2. Тестирование с моками:

package main

import "testing"

// Мок для тестирования
type MockUserRepo struct {
users map[int]*User
}

func NewMockUserRepo() *MockUserRepo {
return &MockUserRepo{
users: map[int]*User{
1: {ID: 1, Name: "John"},
2: {ID: 2, Name: "Jane"},
},
}
}

func (m *MockUserRepo) FindByID(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, fmt.Errorf("user not found")
}

func (m *MockUserRepo) Save(user *User) error {
m.users[user.ID] = user
return nil
}

// Тест использует мок вместо реальной БД
func TestUserService_GetUser(t *testing.T) {
mockRepo := NewMockUserRepo()
service := NewUserService(mockRepo)

user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if user.Name != "John" {
t.Errorf("expected John, got %s", user.Name)
}
}

3. io.Reader и io.Writer:

package main

import (
"bytes"
"io"
"os"
)

// Использование стандартных интерфейсов
func processData(r io.Reader, w io.Writer) error {
// Читаем из любого Reader
data, err := io.ReadAll(r)
if err != nil {
return err
}

// Обрабатываем
processed := bytes.ToUpper(data)

// Пишем в любой Writer
_, err = w.Write(processed)
return err
}

func main() {
// Работа с файлом
file, _ := os.Open("input.txt")
defer file.Close()

var buf bytes.Buffer
processData(file, &buf)

// Работа со строкой
str := bytes.NewReader([]byte("hello world"))
processData(str, os.Stdout)
}

4. Композиция интерфейсов:

package main

import (
"io"
"os"
)

// Расширенный интерфейс
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}

// Реализация
type Buffer struct {
data []byte
}

func (b *Buffer) Read(p []byte) (n int, err error) {
copy(p, b.data)
return len(b.data), nil
}

func (b *Buffer) Write(p []byte) (n int, err error) {
b.data = append(b.data, p...)
return len(p), nil
}

func (b *Buffer) Close() error {
return nil
}

// Функция принимает композитный интерфейс
func ProcessStream(rwc ReadWriteCloser) error {
defer rwc.Close()

data, _ := io.ReadAll(rwc)
rwc.Write(bytes.ToUpper(data))
return nil
}

Нюансы интерфейсов:

package main

import "fmt"

// Nil интерфейс vs интерфейс с nil значением
type MyInterface interface {
DoSomething()
}

type MyStruct struct{}

func (m *MyStruct) DoSomething() {
fmt.Println("Doing something")
}

func main() {
// 1. Nil интерфейс
var i MyInterface
fmt.Println(i == nil) // true

// 2. Интерфейс с nil значением
var s *MyStruct = nil
i = s
fmt.Println(i == nil) // false!

// Это частая ошибка - проверяйте тип перед присвоением
if s != nil {
i = s
}
}

Вывод: Интерфейсы в Go обеспечивают полиморфизм через неявную реализацию. Под капотом интерфейс — это пара (тип, значение). Интерфейсы широко используются для внедрения зависимостей, тестирования и абстракции. Пустой интерфейс interface{} (или any в Go 1.18+) может содержать любой тип.

Вопрос 27. Как устроены интерфейсы в Go?

Таймкод: 00:46:03

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

Правильный ответ:

Определение:

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

Объявление и использование:

package main

import "fmt"

// Определение интерфейса
type Writer interface {
Write(p []byte) (n int, err error)
}

type Reader interface {
Read(p []byte) (n int, err error)
}

// Композиция интерфейсов
type ReadWriter interface {
Reader
Writer
}

// Конкретный тип, реализующий интерфейс
type File struct {
name string
}

// File реализует Writer
func (f *File) Write(p []byte) (n int, err error) {
fmt.Printf("Writing to %s: %s\n", f.name, string(p))
return len(p), nil
}

// File реализует Reader
func (f *File) Read(p []byte) (n int, err error) {
return 0, nil
}

// Функция, принимающая интерфейс
func WriteData(w Writer, data []byte) error {
_, err := w.Write(data)
return err
}

func main() {
file := &File{name: "test.txt"}

// File удовлетворяет Writer
WriteData(file, []byte("Hello"))

// Можно присвоить интерфейсной переменной
var w Writer = file
w.Write([]byte("World"))
}

Устройство интерфейса под капотом:

// Внутреннее представление интерфейса (упрощённо)
type iface struct {
tab *itab // Таблица методов (тип + методы)
data unsafe.Pointer // Указатель на данные
}

type itab struct {
inter *interfacetype // Тип интерфейса
_type *_type // Конкретный тип
hash uint32 // Хэш типа
_ [4]byte
fun [1]uintptr // Массив указателей на методы
}

// Пустой интерфейс (interface{})
type eface struct {
_type *_type // Тип данных
data unsafe.Pointer // Указатель на данные
}

Проверка типов и утверждения:

package main

import "fmt"

type Animal interface {
Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }

type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }

// Type assertion (утверждение типа)
func checkType(a Animal) {
// Безопасная проверка
if dog, ok := a.(Dog); ok {
fmt.Println("It's a dog:", dog.Speak())
}

// Switch по типу
switch v := a.(type) {
case Dog:
fmt.Println("Dog:", v.Speak())
case Cat:
fmt.Println("Cat:", v.Speak())
default:
fmt.Println("Unknown type")
}
}

func main() {
var a Animal = Dog{}
checkType(a)

a = Cat{}
checkType(a)
}

Пустой интерфейс interface{}:

package main

import "fmt"

// interface{} может содержать любой тип
func printValue(v interface{}) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Go 1.18+ используйте any вместо interface{}
func printValueModern(v any) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}

func main() {
printValue(42)
printValue("hello")
printValue(3.14)
printValue([]int{1, 2, 3})
}

Практические примеры:

1. Внедрение зависимостей:

package main

import "fmt"

// Интерфейс для работы с хранилищем
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}

type User struct {
ID int
Name string
}

// Реализация с PostgreSQL
type PostgresRepo struct {
db *sql.DB
}

func (r *PostgresRepo) FindByID(id int) (*User, error) {
// Запрос к PostgreSQL
return nil, nil
}

func (r *PostgresRepo) Save(user *User) error {
// Сохранение в PostgreSQL
return nil
}

// Реализация с MongoDB
type MongoRepo struct {
collection *mongo.Collection
}

func (r *MongoRepo) FindByID(id int) (*User, error) {
// Запрос к MongoDB
return nil, nil
}

func (r *MongoRepo) Save(user *User) error {
// Сохранение в MongoDB
return nil
}

// Сервис зависит от интерфейса, а не от конкретной реализации
type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}

2. Тестирование с моками:

package main

import "testing"

// Мок для тестирования
type MockUserRepo struct {
users map[int]*User
}

func NewMockUserRepo() *MockUserRepo {
return &MockUserRepo{
users: map[int]*User{
1: {ID: 1, Name: "John"},
2: {ID: 2, Name: "Jane"},
},
}
}

func (m *MockUserRepo) FindByID(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, fmt.Errorf("user not found")
}

func (m *MockUserRepo) Save(user *User) error {
m.users[user.ID] = user
return nil
}

// Тест использует мок вместо реальной БД
func TestUserService_GetUser(t *testing.T) {
mockRepo := NewMockUserRepo()
service := NewUserService(mockRepo)

user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if user.Name != "John" {
t.Errorf("expected John, got %s", user.Name)
}
}

3. io.Reader и io.Writer:

package main

import (
"bytes"
"io"
"os"
)

// Использование стандартных интерфейсов
func processData(r io.Reader, w io.Writer) error {
// Читаем из любого Reader
data, err := io.ReadAll(r)
if err != nil {
return err
}

// Обрабатываем
processed := bytes.ToUpper(data)

// Пишем в любой Writer
_, err = w.Write(processed)
return err
}

func main() {
// Работа с файлом
file, _ := os.Open("input.txt")
defer file.Close()

var buf bytes.Buffer
processData(file, &buf)

// Работа со строкой
str := bytes.NewReader([]byte("hello world"))
processData(str, os.Stdout)
}

4. Композиция интерфейсов:

package main

import (
"io"
"os"
)

// Расширенный интерфейс
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}

// Реализация
type Buffer struct {
data []byte
}

func (b *Buffer) Read(p []byte) (n int, err error) {
copy(p, b.data)
return len(b.data), nil
}

func (b *Buffer) Write(p []byte) (n int, err error) {
b.data = append(b.data, p...)
return len(p), nil
}

func (b *Buffer) Close() error {
return nil
}

// Функция принимает композитный интерфейс
func ProcessStream(rwc ReadWriteCloser) error {
defer rwc.Close()

data, _ := io.ReadAll(rwc)
rwc.Write(bytes.ToUpper(data))
return nil
}

Нюансы интерфейсов:

package main

import "fmt"

// Nil интерфейс vs интерфейс с nil значением
type MyInterface interface {
DoSomething()
}

type MyStruct struct{}

func (m *MyStruct) DoSomething() {
fmt.Println("Doing something")
}

func main() {
// 1. Nil интерфейс
var i MyInterface
fmt.Println(i == nil) // true

// 2. Интерфейс с nil значением
var s *MyStruct = nil
i = s
fmt.Println(i == nil) // false!

// Это частая ошибка - проверяйте тип перед присвоением
if s != nil {
i = s
}
}

Вывод: Интерфейсы в Go обеспечивают полиморфизм через неявную реализацию. Под капотом интерфейс — это пара (тип, значение). Интерфейсы широко используются для внедрения зависимостей, тестирования и абстракции. Пустой интерфейс interface{} (или any в Go 1.18+) может содержать любой тип.

Вопрос 28. Как в Go имитировать наследование с помощью структур?

Таймкод: 00:47:54

Ответ собеседника: Правильный. В Go нет классического наследования, но есть композиция через встраивание (embedding) одной структуры в другую. Методы и поля встроенной структуры доступны внешней структуре. Например, структура Fish встраивает структуру Animal, и у Fish доступны все методы и поля Animal. Это не настоящее наследование, а композиция.

Правильный ответ:

Встраивание (Embedding) в Go:

Go не поддерживает классическое наследование, но предоставляет механизм встраивания (embedding), который позволяет создавать композицию структур с доступом к методам и полям встроенных типов.

Базовый пример встраивания:

package main

import "fmt"

// Базовая структура
type Animal struct {
Name string
Age int
}

func (a *Animal) Speak() {
fmt.Printf("%s makes a sound\n", a.Name)
}

func (a *Animal) Info() {
fmt.Printf("Name: %s, Age: %d\n", a.Name, a.Age)
}

// Встраивание Animal в Dog
type Dog struct {
Animal // Встроенная структура (без имени поля)
Breed string
}

// Dog может переопределить метод Animal
func (d *Dog) Speak() {
fmt.Printf("%s barks: Woof!\n", d.Name)
}

func main() {
dog := &Dog{
Animal: Animal{Name: "Rex", Age: 5},
Breed: "Labrador",
}

// Доступ к полям Animal напрямую
fmt.Println(dog.Name) // Rex
fmt.Println(dog.Age) // 5

// Вызов метода Animal
dog.Info() // Name: Rex, Age: 5

// Вызов переопределённого метода
dog.Speak() // Rex barks: Woof!
}

Встраивание указателя:

package main

import "fmt"

type Engine struct {
Power int
}

func (e *Engine) Start() {
fmt.Printf("Engine with %d HP started\n", e.Power)
}

type Car struct {
*Engine // Встраивание указателя
Model string
}

func main() {
car := &Car{
Engine: &Engine{Power: 200},
Model: "BMW",
}

car.Start() // Engine with 200 HP started
fmt.Println(car.Power) // 200
}

Множественное встраивание:

package main

import "fmt"

type Swimmer struct{}

func (s *Swimmer) Swim() {
fmt.Println("Swimming...")
}

type Flyer struct{}

func (f *Flyer) Fly() {
fmt.Println("Flying...")
}

// Duck встраивает оба типа
type Duck struct {
Swimmer
Flyer
}

func main() {
duck := &Duck{}
duck.Swim() // Swimming...
duck.Fly() // Flying...
}

Разрешение конфликтов имён:

package main

import "fmt"

type Logger struct{}

func (l *Logger) Log(msg string) {
fmt.Println("Logger:", msg)
}

type ErrorHandler struct{}

func (e *ErrorHandler) Log(msg string) {
fmt.Println("ErrorHandler:", msg)
}

// Конфликт: оба типа имеют метод Log
type Service struct {
Logger
ErrorHandler
}

func main() {
s := &Service{}

// Ошибка компиляции: ambiguous selector s.Log
// s.Log("test")

// Нужно явно указать, какой метод вызвать
s.Logger.Log("info") // Logger: info
s.ErrorHandler.Log("error") // ErrorHandler: error
}

Практический пример: паттерн Decorator:

package main

import (
"fmt"
"net/http"
)

// Базовый handler
type Handler struct{}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from handler")
}

// Middleware через встраивание
type LoggingHandler struct {
Handler // Встраиваем базовый handler
LogFunc func(string)
}

func (lh *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
lh.LogFunc(fmt.Sprintf("Request: %s %s", r.Method, r.URL.Path))
lh.Handler.ServeHTTP(w, r) // Вызываем базовый handler
}

type MetricsHandler struct {
LoggingHandler
RequestCount int
}

func (mh *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mh.RequestCount++
mh.LoggingHandler.ServeHTTP(w, r)
}

func main() {
handler := &MetricsHandler{
LoggingHandler: LoggingHandler{
LogFunc: func(msg string) {
fmt.Println("[LOG]", msg)
},
},
}

http.Handle("/", handler)
http.ListenAndServe(":8080", nil)
}

Встраивание интерфейсов:

package main

import "fmt"

type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

// Встраивание интерфейсов в структуру
type ReadWriter struct {
Reader
Writer
}

func main() {
rw := &ReadWriter{
Reader: &bytes.Buffer{},
Writer: &bytes.Buffer{},
}

rw.Write([]byte("hello"))
// Работает как Reader и Writer
}

Ограничения встраивания:

package main

import "fmt"

type Base struct {
value int
}

func (b *Base) Get() int {
return b.value
}

type Derived struct {
Base
value int // Скрытие поля Base.value
}

func main() {
d := &Derived{
Base: Base{value: 10},
value: 20,
}

fmt.Println(d.value) // 20 (из Derived)
fmt.Println(d.Base.value) // 10 (из Base)
fmt.Println(d.Get()) // 10 (метод видит Base.value)
}

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

Вопрос 28. Чем массив отличается от слайса в Go?

Таймкод: 00:49:01

Ответ собеседника: Правильный. Массив — статическая структура данных с фиксированным размером, заданным при объявлении. Размер нельзя изменить. Слайс — динамическая структура, основанная на массиве. Содержит ссылку на массив, длину (length) и ёмкость (capacity). Слайс можно расширять, добавлять и удалять элементы, обрезать.

Правильный ответ:

Ключевые отличия:

ХарактеристикаМассивСлайс
РазмерФиксированный при компиляцииДинамический
Тип значениеДа (копируется при присваивании)Нет (ссылочный тип)
Передача в функциюКопируется полностьюПередаётся по ссылке
ИспользованиеРедкоПовсеместно

Массивы:

package main

import "fmt"

func main() {
// Объявление массива
var arr1 [5]int // [0, 0, 0, 0, 0]
arr2 := [3]string{"a", "b", "c"} // С инициализацией
arr3 := [...]int{1, 2, 3} // Размер определяется автоматически

// Размер массива — часть типа
// [3]int и [5]int — разные типы!

// Копирование при присваивании
arr4 := arr2
arr4[0] = "x"
fmt.Println(arr2[0]) // "a" (не изменился)
fmt.Println(arr4[0]) // "x"

// Передача в функцию копирует весь массив
modifyArray(arr3)
fmt.Println(arr3) // [1, 2, 3] (не изменился)
}

func modifyArray(arr [3]int) {
arr[0] = 100
}

Слайсы:

package main

import "fmt"

func main() {
// Объявление слайса
var s1 []int // nil слайс
s2 := []int{1, 2, 3} // С инициализацией
s3 := make([]int, 5) // Длина 5, ёмкость 5
s4 := make([]int, 3, 10) // Длина 3, ёмкость 10

// Слайс — ссылочный тип
s5 := s2
s5[0] = 100
fmt.Println(s2[0]) // 100 (изменился!)

// Передача в функцию по ссылке
modifySlice(s2)
fmt.Println(s2[0]) // 200 (изменился!)
}

func modifySlice(s []int) {
s[0] = 200
}

Внутреннее устройство слайса:

// Внутренняя структура слайса (упрощённо)
type slice struct {
array unsafe.Pointer // Указатель на базовый массив
len int // Текущая длина
cap int // Ёмкость (максимальная длина без реаллокации)
}

func main() {
// Создание слайса
s := make([]int, 3, 5)
// s: [0, 0, 0, _, _]
// ^ ^
// len=3 cap=5

s[0] = 1
s[1] = 2
s[2] = 3

// Добавление элементов
s = append(s, 4) // [1, 2, 3, 4, _] len=4, cap=5
s = append(s, 5) // [1, 2, 3, 4, 5] len=5, cap=5
s = append(s, 6) // Реаллокация! len=6, cap=10 (обычно x2)
}

Операции со слайсами:

package main

import "fmt"

func main() {
s := []int{1, 2, 3, 4, 5}

// Срезы (подслайсы)
fmt.Println(s[1:3]) // [2, 3]
fmt.Println(s[:3]) // [1, 2, 3]
fmt.Println(s[2:]) // [3, 4, 5]
fmt.Println(s[:]) // [1, 2, 3, 4, 5]

// ВНИМАНИЕ: подслайс ссылается на тот же массив!
sub := s[1:3]
sub[0] = 100
fmt.Println(s) // [1, 100, 3, 4, 5] — изменился!

// Чтобы избежать этого, используйте copy или append
subCopy := append([]int{}, s[1:3]...)
subCopy[0] = 200
fmt.Println(s) // [1, 100, 3, 4, 5] — не изменился

// Добавление элементов
s = append(s, 6) // Добавить один
s = append(s, 7, 8, 9) // Добавить несколько
s = append(s, []int{10, 11}...) // Добавить другой слайс

// Удаление элемента
i := 2
s = append(s[:i], s[i+1:]...) // Удалить элемент с индексом 2

// Вставка элемента
s = append(s[:i], append([]int{99}, s[i:]...)...)
}

Ёмкость и реаллокация:

package main

import "fmt"

func main() {
s := make([]int, 0, 3)

for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("len=%d, cap=%d, addr=%p\n", len(s), cap(s), s)
}

// Вывод:
// len=1, cap=3, addr=0x...
// len=2, cap=3, addr=0x...
// len=3, cap=3, addr=0x...
// len=4, cap=6, addr=0x... <- Реаллокация (x2)
// len=5, cap=6, addr=0x...
// len=6, cap=6, addr=0x...
// len=7, cap=12, addr=0x... <- Реаллокация (x2)
// ...
}

Создание слайса из массива:

package main

import "fmt"

func main() {
arr := [5]int{1, 2, 3, 4, 5}

// Слайс ссылается на массив
s := arr[1:4] // [2, 3, 4]

s[0] = 100
fmt.Println(arr) // [1, 100, 3, 4, 5] — массив изменился!

// Если слайс выходит за пределы ёмкости, происходит реаллокация
s = append(s, 6)
s[0] = 200
fmt.Println(arr) // [1, 100, 3, 4, 5] — массив НЕ изменился
}

Сравнение производительности:

package main

import "fmt"

// Массив — копируется при передаче (дорого для больших массивов)
func sumArray(arr [1000]int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}

// Слайс — передаётся по ссылке (дёшево)
func sumSlice(s []int) int {
sum := 0
for _, v := range s {
sum += v
}
return sum
}

func main() {
arr := [1000]int{}
s := make([]int, 1000)

sumArray(arr) // Копирует 1000 элементов
sumSlice(s) // Копирует только заголовок (24 байта)
}

Когда использовать массивы:

package main

// Массивы полезны, когда:
// 1. Размер известен заранее и фиксирован
// 2. Нужна гарантия, что размер не изменится
// 3. Важна производительность (стек вместо кучи)

type Matrix [3][3]float64 // Матрица 3x3

type UUID [16]byte // UUID фиксированного размера

// Ключи в map (массивы можно использовать как ключи)
func main() {
m := map[[3]int]string{
{1, 2, 3}: "value1",
{4, 5, 6}: "value2",
}
_ = m
}

Вывод: Массивы в Go — это типы значений с фиксированным размером, которые копируются при присваивании и передаче в функции. Слайсы — ссылочные типы, построенные на массивах, с динамическим размером. В 95% случаев следует использовать слайсы. Массивы применяются только когда нужна фиксированная размерность или гарантии на этапе компиляции.

Вопрос 29. Как меняется capacity (ёмкость) слайса при его расширении в Go?

Таймкод: 00:49:53

Ответ собеседника: Правильный. Если длина меньше capacity, элемент добавляется в существующий массив без изменения ёмкости. При переполнении создаётся новый массив в два раза больше, данные копируются. Для слайсов больше 1024 элементов ёмкость увеличивается примерно в 1.25 раза (коэффициент ~1.4) для оптимизации памяти.

Правильный ответ:

Стратегия роста ёмкости слайса:

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

Алгоритм роста:

// Упрощённая версия алгоритма из runtime/slice.go
func growslice(oldCap, newCap, cap int, et *_type) int {
// Для маленьких слайсов (< 256 элементов): x2
if oldCap < 256 {
newCap = oldCap * 2
} else {
// Для больших слайсов: x1.25 + некоторая корректировка
for newCap < cap {
newCap += (newCap + 3*256) / 4 // ~1.25x
}
}

// Учитываем размер элемента и ограничения памяти
// ...

return newCap
}

Практические примеры:

package main

import "fmt"

func demonstrateGrowth() {
s := make([]int, 0)

fmt.Println("Рост ёмкости для int:")
for i := 0; i < 2000; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
fmt.Printf("len=%4d, cap=%4d (рост с %d, коэффициент %.2f)\n",
len(s), cap(s), oldCap, float64(cap(s))/float64(oldCap))
}
}
}

func main() {
demonstrateGrowth()
}

Типичный вывод:

len= 1, cap= 1 (рост с 0, коэффициент +Inf)
len= 2, cap= 2 (рост с 1, коэффициент 2.00)
len= 3, cap= 4 (рост с 2, коэффициент 2.00)
len= 5, cap= 8 (рост с 4, коэффициент 2.00)
len= 9, cap= 16 (рост с 8, коэффициент 2.00)
len= 17, cap= 32 (рост с 16, коэффициент 2.00)
len= 33, cap= 64 (рост с 32, коэффициент 2.00)
len= 65, cap= 128 (рост с 64, коэффициент 2.00)
len= 129, cap= 256 (рост с 128, коэффициент 2.00)
len= 257, cap= 512 (рост с 256, коэффициент 2.00)
len= 513, cap= 848 (рост с 512, коэффициент 1.66)
len= 849, cap=1280 (рост с 848, коэффициент 1.51)
len=1281, cap=1792 (рост с 1280, коэффициент 1.40)

Влияние размера элемента:

package main

import "fmt"

type Small struct { x int32 } // 4 байта
type Medium struct { x [16]byte } // 16 байт
type Large struct { x [256]byte } // 256 байт

func main() {
// Для маленьких элементов рост более агрессивный
s1 := make([]Small, 0, 100)
s1 = append(s1, Small{})
s1 = append(s1, Small{})
fmt.Printf("Small: cap=%d\n", cap(s1))

// Для больших элементов рост менее агрессивный
s2 := make([]Large, 0, 100)
s2 = append(s2, Large{})
s2 = append(s2, Large{})
fmt.Printf("Large: cap=%d\n", cap(s2))
}

Оптимизация при известном размере:

package main

import "fmt"

func main() {
// Плохо: множественные реаллокации
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i) // Множественные копирования
}

// Хорошо: предвыделение памяти
s2 := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s2 = append(s2, i) // Без реаллокаций
}

// Ещё лучше: если знаем точный размер
s3 := make([]int, 10000)
for i := 0; i < 10000; i++ {
s3[i] = i // Прямой доступ без append
}

fmt.Printf("s: len=%d, cap=%d\n", len(s), cap(s))
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
fmt.Printf("s3: len=%d, cap=%d\n", len(s3), cap(s3))
}

Сравнение стратегий роста:

package main

import "fmt"

func benchmarkGrowth(strategy string, initialCap, iterations int) {
s := make([]int, 0, initialCap)
totalAllocations := 0

for i := 0; i < iterations; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
totalAllocations++
}
}

fmt.Printf("%s: итераций=%d, реаллокаций=%d, итоговая ёмкость=%d\n",
strategy, iterations, totalAllocations, cap(s))
}

func main() {
// Без предвыделения
benchmarkGrowth("Без предвыделения", 0, 10000)

// С предвыделением x2
benchmarkGrowth("С предвыделением", 10000, 10000)

// Результат:
// Без предвыделения: итераций=10000, реаллокаций=15, итоговая ёмкость=16384
// С предвыделением: итераций=10000, реаллокаций=0, итоговая ёмкость=10000
}

Стоимость реаллокации:

package main

import (
"fmt"
"time"
)

func measureTime(name string, f func()) {
start := time.Now()
f()
elapsed := time.Since(start)
fmt.Printf("%s: %v\n", name, elapsed)
}

func main() {
n := 1_000_000

measureTime("Без предвыделения", func() {
var s []int
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("С предвыделением", func() {
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("Прямая индексация", func() {
s := make([]int, n)
for i := 0; i < n; i++ {
s[i] = i
}
})

// Результат (примерный):
// Без предвыделения: 15ms
// С предвыделением: 8ms
// Прямая индексация: 3ms
}

Особенности реализации в разных версиях Go:

Версия GoПорогСтратегия роста
< 1.141024x2 до 1024, затем x1.25
>= 1.14256x2 до 256, затем x1.25+
>= 1.18256Более сложная формула с учётом размера элемента

Практические рекомендации:

package main

// 1. Используйте make с ёмкостью, если знаете примерный размер
func processItems(items []int) []int {
result := make([]int, 0, len(items)) // Предвыделяем память
for _, item := range items {
if item > 0 {
result = append(result, item)
}
}
return result
}

// 2. Используйте прямую индексацию, если знаете точный размер
func createSequence(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i * i
}
return result
}

// 3. Избегайте append в горячих циклах
func sumSlice(s []int) int {
sum := 0
for _, v := range s { // Быстрее, чем append
sum += v
}
return sum
}

Вывод: Go использует адаптивную стратегию роста ёмкости: x2 для маленьких слайсов (< 256 элементов) и ~x1.25 для больших. Это обеспечивает баланс между использованием памяти и количеством реаллокаций. Для оптимизации производительности рекомендуется предвыделять память с помощью make([]T, 0, capacity), когда известен примерный размер.

Вопрос 29. Как меняется capacity (ёмкость) слайса при его расширении в Go?

Таймкод: 00:49:53

Ответ собеседника: Правильный. Если длина меньше capacity, элемент добавляется в существующий массив без изменения ёмкости. При переполнении создаётся новый массив в два раза больше, данные копируются. Для слайсов больше 1024 элементов ёмкость увеличивается примерно в 1.25 раза (коэффициент ~1.4) для оптимизации памяти.

Правильный ответ:

Стратегия роста ёмкости слайса:

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

Алгоритм роста:

// Упрощённая версия алгоритма из runtime/slice.go
func growslice(oldCap, newCap, cap int, et *_type) int {
// Для маленьких слайсов (< 256 элементов): x2
if oldCap < 256 {
newCap = oldCap * 2
} else {
// Для больших слайсов: x1.25 + некоторая корректировка
for newCap < cap {
newCap += (newCap + 3*256) / 4 // ~1.25x
}
}

// Учитываем размер элемента и ограничения памяти
// ...

return newCap
}

Практические примеры:

package main

import "fmt"

func demonstrateGrowth() {
s := make([]int, 0)

fmt.Println("Рост ёмкости для int:")
for i := 0; i < 2000; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
fmt.Printf("len=%4d, cap=%4d (рост с %d, коэффициент %.2f)\n",
len(s), cap(s), oldCap, float64(cap(s))/float64(oldCap))
}
}
}

func main() {
demonstrateGrowth()
}

Типичный вывод:

len= 1, cap= 1 (рост с 0, коэффициент +Inf)
len= 2, cap= 2 (рост с 1, коэффициент 2.00)
len= 3, cap= 4 (рост с 2, коэффициент 2.00)
len= 5, cap= 8 (рост с 4, коэффициент 2.00)
len= 9, cap= 16 (рост с 8, коэффициент 2.00)
len= 17, cap= 32 (рост с 16, коэффициент 2.00)
len= 33, cap= 64 (рост с 32, коэффициент 2.00)
len= 65, cap= 128 (рост с 64, коэффициент 2.00)
len= 129, cap= 256 (рост с 128, коэффициент 2.00)
len= 257, cap= 512 (рост с 256, коэффициент 2.00)
len= 513, cap= 848 (рост с 512, коэффициент 1.66)
len= 849, cap=1280 (рост с 848, коэффициент 1.51)
len=1281, cap=1792 (рост с 1280, коэффициент 1.40)

Влияние размера элемента:

package main

import "fmt"

type Small struct { x int32 } // 4 байта
type Medium struct { x [16]byte } // 16 байт
type Large struct { x [256]byte } // 256 байт

func main() {
// Для маленьких элементов рост более агрессивный
s1 := make([]Small, 0, 100)
s1 = append(s1, Small{})
s1 = append(s1, Small{})
fmt.Printf("Small: cap=%d\n", cap(s1))

// Для больших элементов рост менее агрессивный
s2 := make([]Large, 0, 100)
s2 = append(s2, Large{})
s2 = append(s2, Large{})
fmt.Printf("Large: cap=%d\n", cap(s2))
}

Оптимизация при известном размере:

package main

import "fmt"

func main() {
// Плохо: множественные реаллокации
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i) // Множественные копирования
}

// Хорошо: предвыделение памяти
s2 := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s2 = append(s2, i) // Без реаллокаций
}

// Ещё лучше: если знаем точный размер
s3 := make([]int, 10000)
for i := 0; i < 10000; i++ {
s3[i] = i // Прямой доступ без append
}

fmt.Printf("s: len=%d, cap=%d\n", len(s), cap(s))
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
fmt.Printf("s3: len=%d, cap=%d\n", len(s3), cap(s3))
}

Сравнение стратегий роста:

package main

import "fmt"

func benchmarkGrowth(strategy string, initialCap, iterations int) {
s := make([]int, 0, initialCap)
totalAllocations := 0

for i := 0; i < iterations; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
totalAllocations++
}
}

fmt.Printf("%s: итераций=%d, реаллокаций=%d, итоговая ёмкость=%d\n",
strategy, iterations, totalAllocations, cap(s))
}

func main() {
// Без предвыделения
benchmarkGrowth("Без предвыделения", 0, 10000)

// С предвыделением x2
benchmarkGrowth("С предвыделением", 10000, 10000)

// Результат:
// Без предвыделения: итераций=10000, реаллокаций=15, итоговая ёмкость=16384
// С предвыделением: итераций=10000, реаллокаций=0, итоговая ёмкость=10000
}

Стоимость реаллокации:

package main

import (
"fmt"
"time"
)

func measureTime(name string, f func()) {
start := time.Now()
f()
elapsed := time.Since(start)
fmt.Printf("%s: %v\n", name, elapsed)
}

func main() {
n := 1_000_000

measureTime("Без предвыделения", func() {
var s []int
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("С предвыделением", func() {
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("Прямая индексация", func() {
s := make([]int, n)
for i := 0; i < n; i++ {
s[i] = i
}
})

// Результат (примерный):
// Без предвыделения: 15ms
// С предвыделением: 8ms
// Прямая индексация: 3ms
}

Особенности реализации в разных версиях Go:

Версия GoПорогСтратегия роста
< 1.141024x2 до 1024, затем x1.25
>= 1.14256x2 до 256, затем x1.25+
>= 1.18256Более сложная формула с учётом размера элемента

Практические рекомендации:

package main

// 1. Используйте make с ёмкостью, если знаете примерный размер
func processItems(items []int) []int {
result := make([]int, 0, len(items)) // Предвыделяем память
for _, item := range items {
if item > 0 {
result = append(result, item)
}
}
return result
}

// 2. Используйте прямую индексацию, если знаете точный размер
func createSequence(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i * i
}
return result
}

// 3. Избегайте append в горячих циклах
func sumSlice(s []int) int {
sum := 0
for _, v := range s { // Быстрее, чем append
sum += v
}
return sum
}

Вывод: Go использует адаптивную стратегию роста ёмкости: x2 для маленьких слайсов (< 256 элементов) и ~x1.25 для больших. Это обеспечивает баланс между использованием памяти и количеством реаллокаций. Для оптимизации производительности рекомендуется предвыделять память с помощью make([]T, 0, capacity), когда известен примерный размер.

Вопрос 29. Как меняется capacity (ёмкость) слайса при его расширении в Go?

Таймкод: 00:49:53

Ответ собеседника: Правильный. Если длина меньше capacity, элемент добавляется в существующий массив без изменения ёмкости. При переполнении создаётся новый массив в два раза больше, данные копируются. Для слайсов больше 1024 элементов ёмкость увеличивается примерно в 1.25 раза (коэффициент ~1.4) для оптимизации памяти.

Правильный ответ:

Стратегия роста ёмкости слайса:

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

Алгоритм роста:

// Упрощённая версия алгоритма из runtime/slice.go
func growslice(oldCap, newCap, cap int, et *_type) int {
// Для маленьких слайсов (< 256 элементов): x2
if oldCap < 256 {
newCap = oldCap * 2
} else {
// Для больших слайсов: x1.25 + некоторая корректировка
for newCap < cap {
newCap += (newCap + 3*256) / 4 // ~1.25x
}
}

// Учитываем размер элемента и ограничения памяти
// ...

return newCap
}

Практические примеры:

package main

import "fmt"

func demonstrateGrowth() {
s := make([]int, 0)

fmt.Println("Рост ёмкости для int:")
for i := 0; i < 2000; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
fmt.Printf("len=%4d, cap=%4d (рост с %d, коэффициент %.2f)\n",
len(s), cap(s), oldCap, float64(cap(s))/float64(oldCap))
}
}
}

func main() {
demonstrateGrowth()
}

Типичный вывод:

len= 1, cap= 1 (рост с 0, коэффициент +Inf)
len= 2, cap= 2 (рост с 1, коэффициент 2.00)
len= 3, cap= 4 (рост с 2, коэффициент 2.00)
len= 5, cap= 8 (рост с 4, коэффициент 2.00)
len= 9, cap= 16 (рост с 8, коэффициент 2.00)
len= 17, cap= 32 (рост с 16, коэффициент 2.00)
len= 33, cap= 64 (рост с 32, коэффициент 2.00)
len= 65, cap= 128 (рост с 64, коэффициент 2.00)
len= 129, cap= 256 (рост с 128, коэффициент 2.00)
len= 257, cap= 512 (рост с 256, коэффициент 2.00)
len= 513, cap= 848 (рост с 512, коэффициент 1.66)
len= 849, cap=1280 (рост с 848, коэффициент 1.51)
len=1281, cap=1792 (рост с 1280, коэффициент 1.40)

Влияние размера элемента:

package main

import "fmt"

type Small struct { x int32 } // 4 байта
type Medium struct { x [16]byte } // 16 байт
type Large struct { x [256]byte } // 256 байт

func main() {
// Для маленьких элементов рост более агрессивный
s1 := make([]Small, 0, 100)
s1 = append(s1, Small{})
s1 = append(s1, Small{})
fmt.Printf("Small: cap=%d\n", cap(s1))

// Для больших элементов рост менее агрессивный
s2 := make([]Large, 0, 100)
s2 = append(s2, Large{})
s2 = append(s2, Large{})
fmt.Printf("Large: cap=%d\n", cap(s2))
}

Оптимизация при известном размере:

package main

import "fmt"

func main() {
// Плохо: множественные реаллокации
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i) // Множественные копирования
}

// Хорошо: предвыделение памяти
s2 := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s2 = append(s2, i) // Без реаллокаций
}

// Ещё лучше: если знаем точный размер
s3 := make([]int, 10000)
for i := 0; i < 10000; i++ {
s3[i] = i // Прямой доступ без append
}

fmt.Printf("s: len=%d, cap=%d\n", len(s), cap(s))
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
fmt.Printf("s3: len=%d, cap=%d\n", len(s3), cap(s3))
}

Сравнение стратегий роста:

package main

import "fmt"

func benchmarkGrowth(strategy string, initialCap, iterations int) {
s := make([]int, 0, initialCap)
totalAllocations := 0

for i := 0; i < iterations; i++ {
oldCap := cap(s)
s = append(s, i)
if cap(s) != oldCap {
totalAllocations++
}
}

fmt.Printf("%s: итераций=%d, реаллокаций=%d, итоговая ёмкость=%d\n",
strategy, iterations, totalAllocations, cap(s))
}

func main() {
// Без предвыделения
benchmarkGrowth("Без предвыделения", 0, 10000)

// С предвыделением x2
benchmarkGrowth("С предвыделением", 10000, 10000)

// Результат:
// Без предвыделения: итераций=10000, реаллокаций=15, итоговая ёмкость=16384
// С предвыделением: итераций=10000, реаллокаций=0, итоговая ёмкость=10000
}

Стоимость реаллокации:

package main

import (
"fmt"
"time"
)

func measureTime(name string, f func()) {
start := time.Now()
f()
elapsed := time.Since(start)
fmt.Printf("%s: %v\n", name, elapsed)
}

func main() {
n := 1_000_000

measureTime("Без предвыделения", func() {
var s []int
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("С предвыделением", func() {
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}
})

measureTime("Прямая индексация", func() {
s := make([]int, n)
for i := 0; i < n; i++ {
s[i] = i
}
})

// Результат (примерный):
// Без предвыделения: 15ms
// С предвыделением: 8ms
// Прямая индексация: 3ms
}

Особенности реализации в разных версиях Go:

Версия GoПорогСтратегия роста
< 1.141024x2 до 1024, затем x1.25
>= 1.14256x2 до 256, затем x1.25+
>= 1.18256Более сложная формула с учётом размера элемента

Практические рекомендации:

package main

// 1. Используйте make с ёмкостью, если знаете примерный размер
func processItems(items []int) []int {
result := make([]int, 0, len(items)) // Предвыделяем память
for _, item := range items {
if item > 0 {
result = append(result, item)
}
}
return result
}

// 2. Используйте прямую индексацию, если знаете точный размер
func createSequence(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i * i
}
return result
}

// 3. Избегайте append в горячих циклах
func sumSlice(s []int) int {
sum := 0
for _, v := range s { // Быстрее, чем append
sum += v
}
return sum
}

Вывод: Go использует адаптивную стратегию роста ёмкости: x2 для маленьких слайсов (< 256 элементов) и ~x1.25 для больших. Это обеспечивает баланс между использованием памяти и количеством реаллокаций. Для оптимизации производительности рекомендуется предвыделять память с помощью make([]T, 0, capacity), когда известен примерный размер.

Вопрос 30. Чем отличается буферизированный канал от небуферизированного в Go?

Таймкод: 00:54:33

Ответ собеседника: Правильный. Небуферизированный канал не имеет буфера — горутина-отправитель блокируется до тех пор, пока горутина-получатель не прочитает значение. Буферизированный канал имеет буфер (массив заданного размера). Горутина-отправитель не блокируется, пока буфер не заполнится. При заполнении буфера отправитель блокируется до чтения. При чтении буфер уменьшается, и заблокированный отправитель разблокируется.

Правильный ответ:

Небуферизированный канал (unbuffered channel):

package main

import (
"fmt"
"time"
)

func main() {
// Создание небуферизированного канала
ch := make(chan int)

// Горутина-отправитель
go func() {
fmt.Println("Отправитель: отправляю значение...")
ch <- 42 // Блокируется, пока получатель не прочитает
fmt.Println("Отправитель: значение отправлено!")
}()

// Имитация задержки
time.Sleep(time.Second)

// Горутина-получатель (main)
fmt.Println("Получатель: читаю значение...")
value := <-ch // Блокируется, пока отправитель не отправит
fmt.Printf("Получатель: получено %d\n", value)

// Вывод:
// Отправитель: отправляю значение...
// (пауза 1 секунда)
// Получатель: читаю значение...
// Отправитель: значение отправлено!
// Получатель: получено 42
}

Буферизированный канал (buffered channel):

package main

import "fmt"

func main() {
// Создание буферизированного канала с ёмкостью 3
ch := make(chan int, 3)

// Можно отправить 3 значения без блокировки
ch <- 1
fmt.Println("Отправлено 1, буфер: 1/3")

ch <- 2
fmt.Println("Отправлено 2, буфер: 2/3")

ch <- 3
fmt.Println("Отправлено 3, буфер: 3/3")

// ch <- 4 // Блокировка! Буфер полон

// Читаем значения
fmt.Println("Получено:", <-ch) // 1
fmt.Println("Получено:", <-ch) // 2
fmt.Println("Получено:", <-ch) // 3

// Теперь можно снова отправлять
ch <- 4 // Работает, так как буфер освободился
fmt.Println("Отправлено 4")
}

Сравнение:

package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("=== Небуферизированный канал ===")
unbuffered := make(chan int)

start := time.Now()
go func() {
unbuffered <- 1
fmt.Printf("Отправлено через %v\n", time.Since(start))
}()

time.Sleep(100 * time.Millisecond) // Задержка перед чтением
<-unbuffered
fmt.Printf("Прочитано через %v\n", time.Since(start))

fmt.Println("\n=== Буферизированный канал ===")
buffered := make(chan int, 1)

start = time.Now()
go func() {
buffered <- 1
fmt.Printf("Отправлено через %v\n", time.Since(start))
}()

time.Sleep(100 * time.Millisecond) // Задержка перед чтением
<-buffered
fmt.Printf("Прочитано через %v\n", time.Since(start))

// Вывод:
// === Небуферизированный канал ===
// Отправлено через 100ms (заблокировался до чтения)
// Прочитано через 100ms
//
// === Буферизированный канал ===
// Отправлено через ~0ms (не блокировался)
// Прочитано через 100ms
}

Практические примеры использования:

1. Паттерн Worker Pool с буферизированным каналом:

package main

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

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d: обрабатываю job %d\n", id, job)
time.Sleep(100 * time.Millisecond) // Имитация работы
results <- job * 2
}
}

func main() {
const numJobs = 10
const numWorkers = 3

// Буферизированный канал для задач
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)

var wg sync.WaitGroup

// Запускаем воркеров
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}

// Отправляем задачи (не блокируется, пока буфер не полон)
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)

// Ждём завершения всех воркеров
wg.Wait()
close(results)

// Собираем результаты
for result := range results {
fmt.Printf("Результат: %d\n", result)
}
}

2. Rate Limiting с буферизированным каналом:

package main

import (
"fmt"
"time"
)

// RateLimiter ограничивает количество запросов в секунду
type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
}

func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, rate),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
}

// Инициализируем токены
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}

// Пополняем токены
go func() {
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default: // Буфер полон, пропускаем
}
}
}()

return rl
}

func (rl *RateLimiter) Wait() {
<-rl.tokens
}

func (rl *RateLimiter) Stop() {
rl.ticker.Stop()
}

func main() {
limiter := NewRateLimiter(5) // 5 запросов в секунду
defer limiter.Stop()

for i := 0; i < 10; i++ {
limiter.Wait()
fmt.Printf("Запрос %d выполнен в %v\n", i+1, time.Now())
}
}

3. Семафор с буферизированным каналом:

package main

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

// Semaphore ограничивает количество одновременных операций
type Semaphore struct {
sem chan struct{}
}

func NewSemaphore(maxConcurrent int) *Semaphore {
return &Semaphore{
sem: make(chan struct{}, maxConcurrent),
}
}

func (s *Semaphore) Acquire() {
s.sem <- struct{}{}
}

func (s *Semaphore) Release() {
<-s.sem
}

func main() {
sem := NewSemaphore(3) // Максимум 3 одновременных операции

var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

sem.Acquire() // Захватываем семафор
defer sem.Release() // Освобождаем при выходе

fmt.Printf("Горутина %d: начала работу\n", id)
time.Sleep(time.Second)
fmt.Printf("Горутина %d: завершила работу\n", id)
}(i)
}

wg.Wait()
}

4. Небуферизированный канал для синхронизации:

package main

import "fmt"

func main() {
done := make(chan struct{}) // Небуферизированный канал

go func() {
fmt.Println("Горутина: выполняю работу...")
// ... работа ...
close(done) // Сигнал о завершении
}()

<-done // Блокировка до завершения горутины
fmt.Println("Main: горутина завершилась")
}

Когда использовать каждый тип:

package main

// Небуферизированный канал:
// 1. Гарантированная синхронизация между горутинами
// 2. Сигнализация о событиях
// 3. Передача данных с гарантией доставки

func eventNotification() {
event := make(chan struct{})

go func() {
// Ждём событие
<-event
fmt.Println("Событие получено!")
}()

// Отправляем событие
close(event)
}

// Буферизированный канал:
// 1. Разделение скоростей отправителя и получателя
// 2. Пакетная обработка
// 3. Rate limiting
// 4. Worker pools

func batchProcessing() {
const batchSize = 100
batch := make(chan int, batchSize)

go func() {
for item := range batch {
process(item)
}
}()

// Отправляем пакет данных
for i := 0; i < batchSize; i++ {
batch <- i
}
close(batch)
}

func process(item int) {
// Обработка элемента
}

Вывод: Небуферизированные каналы обеспечивают строгую синхронизацию — отправитель блокируется до получения. Буферизированные каналы позволяют отправлять данные без блокировки, пока буфер не заполнится. Выбор зависит от задачи: небуферизированные для синхронизации, буферизированные для разделения скоростей и пакетной обработки.