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

Самое полное интервью Golang Middle

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

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

Вопрос 1. Как вы перешли с C# на Go и какие различия между языками заметили?

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

Ответ собеседника: Правильный. Работа на C# была связана с узкоспециализированной разработкой плагинов для определённой системы, где всё развивалось медленно и проект был очень долгим. Решил перейти на Go из-за активного комьюнити, быстрого развития языка и больших перспектив на рынке труда.

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

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

1. Модель конкурентности

C# использует async/await поверх Task Parallel Library — это модель асинхронного программирования с контекстом синхронизации, SynchronizationContext, ConfigureAwait и прочими нюансами. Go предлагает принципиально иной подход — горутины (goroutines) и каналы (channels). Горутины — это легковесные потоки, управляемые рантаймом Go, а не ОС. Один процесс Go может запускать сотни тысяч горутин без проблем с памятью.

// Go: запуск горутины и передача данных через канал
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)

for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}

for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)

for a := 1; a <= 5; a++ {
<-results
}
}

В C# аналог потребовал бы Task, async/await, а для похожей модели «воркеров» — настройки планировщика задач.

2. Система типов и ООП

C# — классический ООП-язык с наследованием, классами, интерфейсами, дженериками (с мощной системой ограничений), свойствами, событиями, LINQ. Go намеренно отказался от наследования. Вместо этого — композиция и интерфейсы, которые реализуются неявно (duck typing на этапе компиляции).

// Go: интерфейс реализуется неявно — не нужно писать "implements"
type Writer interface {
Write(p []byte) (n int, err error)
}

type FileWriter struct{}

func (fw FileWriter) Write(p []byte) (int, error) {
// запись в файл
return len(p), nil
}

// FileWriter автоматически удовлетворяет интерфейсу Writer
// без явного объявления

В C# пришлось бы явно указать : IWriter, а для множественного наследования интерфейсов — использовать сложные иерархии или default-методы интерфейсов.

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

C# использует исключения (exceptions) — try/catch/finally. Go использует явный возврат ошибок как значений. Это фундаментальное различие, которое влияет на архитектуру кода.

// Go: ошибки — это значения, которые нужно явно проверять
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}

func main() {
result, err := divide(10, 0)
if err != nil {
log.Printf("Error: %v", err)
return
}
fmt.Println(result)
}

В C# аналог выбросил бы DivideByZeroException, и его можно было бы «поймать» на любом уровне стека вызовов. В Go ошибка должна быть обработана явно на каждом уровне или проброшена вверх — это делает код более предсказуемым, но многословным.

4. Управление памятью

Оба языка имеют сборщик мусора, но подходы различаются. C# использует поколенческий GC (Gen 0, 1, 2) с поддержкой LOH (Large Object Heap). Go использует маркировку-и-очистку (mark-and-sweep) с низкой задержкой (low-latency GC), оптимизированный для короткоживущих объектов. В Go нет поколений (до Go 1.22), но есть эскалация и инкрементальный GC.

5. Компиляция и развёртывание

C# компилируется в IL-код, который выполняется на CLR (Common Language Runtime). Для работы нужен .NET Runtime. Go компилируется в статически слинкованный бинарный файл без внешних зависимостей. Это делает деплой Go-приложений значительно проще — один файл, запустил и работает.

# Кросс-компиляция в Go — одна команда
GOOS=linux GOARCH=amd64 go build -o myapp

# Результат — один бинарник, который можно запустить на любом Linux

6. Инструменты и экосистема

C# имеет Visual Studio, ReSharper, мощную экосистему NuGet. Go имеет встроенные инструменты: go fmt (форматирование), go test (тестирование), go vet (статический анализ), go mod (управление зависимостями). Философия Go — «один правильный способ сделать что-то», что снижает когнитивную нагрузку при переходе между проектами.

7. Дженерики

C# имеет дженерики с версии 2.0 (2005 год). Go получил дженерики только в версии 1.18 (2022 год). Дженерики в Go проще и менее мощные — нет ограничений по типам, как в C#, но есть type constraints.

// Go: дженерики с ограничением типов
type Number interface {
~int | ~int64 | ~float64
}

func Sum[T Number](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}

Итог: переход с C# на Go — это переход от «всё включено» к «минимализм и явность». Go заставляет писать более простой и предсказуемый код, но требует отказа от привычных ООП-паттернов и принятия идиом языка.

Вопрос 2. Что удалось интересного сделать за последние полгода на Go и что понравилось?

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

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

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

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

1. Быстрый цикл обратной связи

Одно из ключевых преимуществ Go — скорость компиляции и деплоя. Это позволяет реализовать полный цикл «код → тест → деплой → обратная связь» за минуты, а не часы. Например, при разработке микросервиса на Go с использованием Docker и Kubernetes:

// Пример простого HTTP-сервиса, который можно задеплоить за минуты
package main

import (
"encoding/json"
"log"
"net/http"
)

type Response struct {
Status string `json:"status"`
Version string `json:"version"`
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
resp := Response{
Status: "ok",
Version: "1.0.0",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}

func main() {
http.HandleFunc("/health", healthHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
# Multi-stage build — минимальный образ
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY ../blog-draft .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server

FROM alpine:latest
COPY --from=builder /server /server
EXPOSE 8080
CMD ["/server"]

Итоговый образ — около 15-20 МБ, запуск за секунды. Это позволяет быстро итерировать и получать обратную связь.

2. Конкурентность в реальных задачах

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

// Параллельный сбор данных из нескольких API
type Result struct {
Source string
Data interface{}
Err error
}

func fetchFromSource(ctx context.Context, source string) Result {
// Имитация запроса к внешнему API
req, _ := http.NewRequestWithContext(ctx, "GET", source, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return Result{Source: source, Err: err}
}
defer resp.Body.Close()

var data interface{}
json.NewDecoder(resp.Body).Decode(&data)
return Result{Source: source, Data: data}
}

func aggregateData(ctx context.Context, sources []string) []Result {
results := make([]Result, len(sources))
var wg sync.WaitGroup

for i, source := range sources {
wg.Add(1)
go func(i int, s string) {
defer wg.Done()
results[i] = fetchFromSource(ctx, s)
}(i, source)
}

wg.Wait()
return results
}

3. Мониторинг и наблюдаемость

Go имеет отличную поддержку метрик и трейсинг. Использование Prometheus и OpenTelemetry позволяет быстро получить представление о работе сервиса:

// Интеграция с Prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duration of HTTP requests",
},
[]string{"path", "method"},
)
)

func init() {
prometheus.MustRegister(requestDuration)
}

func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
requestDuration.WithLabelValues(r.URL.Path, r.Method).
Observe(time.Since(start).Seconds())
})
}

4. Тестирование

Встроенная поддержка тестирования в Go позволяет быстро писать тесты и получать уверенность в работоспособности кода:

// Пример теста с табличным подходом
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
hasError bool
}{
{"normal division", 10, 2, 5, false},
{"division by zero", 10, 0, 0, true},
{"negative numbers", -10, 2, -5, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := divide(tt.a, tt.b)
if tt.hasError {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("expected %f, got %f", tt.expected, result)
}
})
}
}

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

Вопрос 3. Чем модель ООП в Go отличается от C#/Java в плане наследования и полиморфизма?

Таймкод: 00:03:30

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

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

Go намеренно отказался от классической ООП-модели, которую мы знаем из C# и Java. Это не ограничение, а осознанный архитектурный выбор, который решает множество проблем, присущих глубоким иерархиям наследования.

1. Отсутствие наследования — проблема ромбовидного наследования

В C# и Java множественное наследование классов запрещено именно из-за проблемы ромбовидного наследования (diamond problem). Go решил эту проблему радикально — убрав наследование классов полностью.

// C#: Проблема ромбовидного наследования
// Это невозможно в C#:
class A { public void Method() {} }
class B : A { }
class C : A { }
// class D : B, C { } // Ошибка компиляции!

В Go такой проблемы просто не существует, потому что наследования нет.

2. Встраивание (Embedding) вместо наследования

Go использует композицию через встраивание структур. Это не наследование — встроенные методы не получают доступ к полям структуры-обёртки, и нет отношения «является» (is-a).

// Go: встраивание структур
type Logger struct {
prefix string
}

func (l Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.prefix, message)
}

type Service struct {
Logger // Встраивание — методы Logger становятся методами Service
name string
}

func main() {
s := Service{
Logger: Logger{prefix: "SERVICE"},
name: "MyService",
}
s.Log("started") // Вызов метода Logger через Service
}

Важно понимать: встраивание — это синтаксический сахар для делегирования, а не наследование. Метод Log вызывается на экземпляре Logger, а не на Service.

3. Неявная реализация интерфейсов (Structural Typing)

В C# и Java класс должен явно указывать, какие интерфейсы он реализует. В Go тип автоматически реализует интерфейс, если имеет все необходимые методы. Это называется structural typing или duck typing на этапе компиляции.

// Go: интерфейс реализуется неявно
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

// File автоматически реализует Reader и Writer
// без явного объявления
type File struct{}

func (f File) Read(p []byte) (int, error) {
return len(p), nil
}

func (f File) Write(p []byte) (int, error) {
return len(p), nil
}

// Можно использовать File как Reader, Writer или ReadWriter
var r Reader = File{}
var w Writer = File{}

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

4. Полиморфизм через интерфейсы

Полиморфизм в Go работает через интерфейсы, но с важным отличием — интерфейсы обычно маленькие и определяются на стороне потребителя, а не поставщика.

// Go: интерфейс определяется там, где он используется
package storage

// Интерфейс определён в пакете, который его использует
type UserStore interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}

type Service struct {
store UserStore
}

func NewService(store UserStore) *Service {
return &Service{store: store}
}

// Реализация может быть в другом пакете
package postgres

type PostgresStore struct {
db *sql.DB
}

func (p *PostgresStore) GetUser(id int) (*User, error) {
// реализация
}

func (p *PostgresStore) SaveUser(user *User) error {
// реализация
}

В C# пришлось бы создавать интерфейс IUserStore в отдельной сборке или в пакете реализации, что создаёт зависимости.

5. Отсутствие конструкторов и деструкторов

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

// Go: фабричная функция вместо конструктора
type Server struct {
addr string
timeout time.Duration
}

func NewServer(addr string) *Server {
return &Server{
addr: addr,
timeout: 30 * time.Second,
}
}

func NewServerWithTimeout(addr string, timeout time.Duration) *Server {
return &Server{
addr: addr,
timeout: timeout,
}
}

Деструкторов тоже нет — вместо них используется интерфейс io.Closer или пакет runtime.SetFinalizer (который не рекомендуется использовать).

6. Сравнение подходов

АспектC#/JavaGo
НаследованиеЕсть (одиночное)Нет
КомпозицияЕстьЕсть (встраивание)
ИнтерфейсыЯвная реализацияНеявная реализация
ИерархииГлубокиеПлоские
ПолиморфизмЧерез наследование и интерфейсыТолько через интерфейсы

Итог: Go заменяет наследование композицией и встраиванием, а полиморфизм реализует через неявные интерфейсы. Это устраняет проблемы глубоких иерархий, делает код более гибким и тестируемым, но требует изменения мышления при проектировании.

Вопрос 4. Как вы относитесь к появлению дженериков в Go?

Таймкод: 00:05:05

Ответ собеседника: Правильный. Дженерики — это полезный инструмент, решающий много задач. Есть небольшие опасения из-за проблем с IDE, но в целом инструмент очень нужный. Не хотелось бы, чтобы Go превратился в «мини-Java». Пока дженериками не пользовался активно, только изучал.

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

Дженерики появились в Go 1.18 (март 2022) после более чем 10 лет дискуссий. Это одно из самых значительных изменений в языке, и отношение к ним в сообществе неоднозначное.

1. Проблема, которую решают дженерики

До дженериков для создания универсальных функций приходилось использовать interface{}, что приводило к потере типобезопасности и необходимости приведения типов:

// До дженериков: небезопасно и многословно
func MinInt(a, b int) int {
if a < b {
return a
}
return b
}

func MinFloat(a, b float64) float64 {
if a < b {
return a
}
return b
}

// Или через interface{} — теряем типобезопасность
func Min(a, b interface{}) interface{} {
// Нужны type assertions или reflect
}

С дженериками:

// С дженериками: одна функция для всех сравнимых типов
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}

// Использование
minInt := Min(3, 5) // T выводится как int
minFloat := Min(3.14, 2.72) // T выводится как float64
minStr := Min("abc", "xyz") // T выводится как string

2. Типовые параметры и ограничения (constraints)

Дженерики в Go используют систему ограничений (constraints) для определения допустимых типов:

// Ограничение через объединение типов
type Number interface {
~int | ~int64 | ~float64
}

// Ограничение через интерфейс
type Stringer interface {
String() string
}

// Ограничение через метод
type Printable interface {
~struct{ Name string }
}

func Sum[T Number](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}

3. Практические примеры использования

Дженерики особенно полезны для создания универсальных структур данных и утилит:

// Универсальный кэш
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
items: make(map[K]V),
}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}

func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}

// Универсальный результат с ошибкой
type Result[T any] struct {
Value T
Err error
}

func (r Result[T]) Unwrap() (T, error) {
return r.Value, r.Err
}

4. Опасения и ограничения

Справедливые опасения, которые вы упомянули:

  • Сложность кода: Чрезмерное использование дженериков может сделать код трудночитаемым, особенно с вложенными типами и ограничениями.
  • Время компиляции: Дженерики могут увеличивать время компиляции, особенно при сложных ограничениях.
  • Размер бинарника: Мономорфизация (создание отдельных копий для каждого типа) может увеличивать размер бинарного файла.
  • IDE-поддержка: Некоторые IDE могут хуже справляться с выводом типов и автодополнением при работе с дженериками.

5. Рекомендации по использованию

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

6. Сравнение с C# дженериками

Дженерики в Go проще, чем в C#:

  • Нет ограничений по типам значений/ссылок
  • Нет variance (in/out)
  • Нет статических членов в дженериках
  • Нет рекурсивных ограничений

Это сделано намеренно — Go стремится к простоте, а не к максимальной выразительности.

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

Вопрос 5. Что такое интерфейс в Go и чем он отличается от интерфейса в Java/C#?

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

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

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

Интерфейсы в Go — это одна из самых мощных и элегантных особенностей языка. Они реализуют принцип «программируйте к интерфейсу, а не к реализации» на практике, а не на словах.

1. Структура интерфейса в Go

Интерфейс в Go — это набор сигнатур методов. Внутри он представлен как пара указателей: на информацию о типе и на значение.

// Определение интерфейса
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

// Композиция интерфейсов
type ReadWriter interface {
Reader
Writer
}

2. Неявная реализация — ключевое отличие

В Java/C# класс должен явно объявить, что реализует интерфейс. В Go — нет. Это фундаментальное различие, которое меняет подход к проектированию.

// Go: неявная реализация
type File struct {
name string
}

func (f *File) Read(p []byte) (int, error) {
// реализация чтения
return len(p), nil
}

func (f *File) Write(p []byte) (int, error) {
// реализация записи
return len(p), nil
}

// File автоматически реализует Reader, Writer и ReadWriter
// без какого-либо объявления
var r Reader = &File{name: "test.txt"}
var w Writer = &File{name: "test.txt"}
var rw ReadWriter = &File{name: "test.txt"}
// C#: явная реализация
public class File : IReader, IWriter, IReadWriter
{
public int Read(byte[] p) { /* ... */ }
public int Write(byte[] p) { /* ... */ }
}

3. Интерфейсы определяются потребителем

В Go принято определять интерфейсы там, где они используются, а не там, где они реализуются. Это инверсия подхода из C#/Java.

// Пакет бизнес-логики определяет нужный ему интерфейс
package service

type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}

type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

// Пакет инфраструктуры реализует интерфейс — без знания о нём
package postgres

type PostgresUserRepo struct {
db *sql.DB
}

func (p *PostgresUserRepo) FindByID(id int) (*User, error) {
// SQL-запрос
row := p.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
// ...
}

func (p *PostgresUserRepo) Save(user *User) error {
// SQL-запрос
_, err := p.db.Exec("INSERT INTO users ...", user.Name)
return err
}

4. Пустой интерфейс interface{} (any)

Пустой интерфейс не требует методов — ему соответствует любой тип. В Go 1.18 появился алиас any:

// interface{} и any — это одно и то же
func PrintValue(v any) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Использование
PrintValue(42)
PrintValue("hello")
PrintValue(struct{ Name string }{Name: "Go"})

5. Type assertion и type switch

Для извлечения конкретного типа из интерфейса используются type assertion и type switch:

// Type assertion
var r Reader = &File{name: "test.txt"}
file := r.(*File) // паника, если тип не File

// Безопасный type assertion
if file, ok := r.(*File); ok {
fmt.Println(file.name)
} else {
fmt.Println("Not a File")
}

// Type switch
func describeValue(v any) {
switch val := v.(type) {
case int:
fmt.Printf("Integer: %d\n", val)
case string:
fmt.Printf("String: %s\n", val)
case error:
fmt.Printf("Error: %v\n", val)
default:
fmt.Printf("Unknown type: %T\n", val)
}
}

6. Внутреннее представление интерфейса

Интерфейс в Go — это пара указателей:

// Упрощённая структура интерфейса
type iface struct {
tab *itab // информация о типе и методах
data unsafe.Pointer // указатель на данные
}

Когда вы присваиваете значение интерфейсу, Go создаёт itab (interface table) с информацией о типе и указателями на методы.

7. Практические паттерны

Паттерн «Accept interfaces, return structs»:

// Правильно: функция принимает интерфейс, возвращает конкретный тип
func NewClient(baseURL string) *http.Client {
return &http.Client{
Transport: &http.Transport{
// настройки
},
}
}

// Правильно: метод принимает интерфейс
func ProcessData(r Reader) error {
buf := make([]byte, 1024)
n, err := r.Read(buf)
// обработка
return err
}

Маленькие интерфейсы:

// Go поощряет маленькие интерфейсы
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

// Композиция при необходимости
type ReadCloser interface {
Reader
Closer
}

Итог: Интерфейсы в Go — это инструмент для определения поведения, а не для классификации объектов. Неявная реализация и определение интерфейсов на стороне потребителя делают код более гибким, тестируемым и слабо связанным. Это одна из главных причин, почему Go так хорош для создания тестируемого кода.

Вопрос 6. Где лучше объявлять интерфейс — рядом со структурой или в месте использования?

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

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

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

Это один из самых обсуждаемых вопросов в Go-сообществе. Ответ зависит от контекста, но есть чёткие рекомендации, подтверждённые практикой.

1. Общее правило: интерфейсы определяет потребитель

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

// Пакет service — потребитель
package service

// UserRepository — интерфейс, определённый потребителем
// Содержит ТОЛЬКО методы, которые нужны service
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Save(ctx context.Context, user *User) error
}

type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Пакет postgres — поставщик
package postgres

// PostgresUserRepo реализует service.UserRepository
// без явного объявления
type PostgresUserRepo struct {
db *sql.DB
}

func (p *PostgresUserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
row := p.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
// ...
}

func (p *PostgresUserRepo) Save(ctx context.Context, user *User) error {
_, err := p.db.ExecContext(ctx, "INSERT INTO users ...", user.Name)
return err
}

// Может иметь и другие методы, не относящиеся к интерфейсу
func (p *PostgresUserRepo) Delete(ctx context.Context, id int64) error {
// ...
}

2. Преимущества определения на стороне потребителя

  • Минимальная зависимость: Потребитель зависит только от того, что ему нужно.
  • Тестируемость: Легко создать mock для маленького интерфейса.
  • Гибкость: Можно подменить реализацию без изменения интерфейса.
  • Отсутствие зависимостей: Поставщик не знает о существовании потребителя.

3. Когда интерфейс определяет поставщик

Бывают случаи, когда интерфейс логично определить на стороне поставщика:

  • Публичные библиотеки и SDK: Когда вы создаёте библиотеку, которую будут использовать другие.
  • API с несколькими реализациями: Когда заранее известно, что будет несколько реализаций.
  • Стандартные паттерны: Например, io.Reader, io.Writer, http.Handler.
// Публичная библиотека определяет интерфейс
package cache

type Cache interface {
Get(ctx context.Context, key string) ([]byte, bool)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, key string) error
}

// Реализации
func NewInMemoryCache() Cache { /* ... */ }
func NewRedisCache(addr string) Cache { /* ... */ }

4. Практический пример: тестирование

Определение интерфейса на стороне потребителя делает тестирование тривиальным:

// Продакшн-код
package service

type EmailSender interface {
Send(ctx context.Context, to, subject, body string) error
}

type NotificationService struct {
sender EmailSender
}

func (n *NotificationService) NotifyUser(ctx context.Context, user *User) error {
return n.sender.Send(ctx, user.Email, "Welcome!", "Hello!")
}
// Тестовый код
package service

type mockEmailSender struct {
sent []email
}

func (m *mockEmailSender) Send(ctx context.Context, to, subject, body string) error {
m.sent = append(m.sent, email{to, subject, body})
return nil
}

func TestNotificationService_NotifyUser(t *testing.T) {
mock := &mockEmailSender{}
svc := &NotificationService{sender: mock}

user := &User{Email: "test@example.com"}
err := svc.NotifyUser(context.Background(), user)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(mock.sent) != 1 {
t.Fatalf("expected 1 email, got %d", len(mock.sent))
}

if mock.sent[0].to != "test@example.com" {
t.Errorf("expected email to test@example.com, got %s", mock.sent[0].to)
}
}

5. Антипаттерн: интерфейс со всеми методами структуры

// ПЛОХО: интерфейс дублирует все методы структуры
type UserServiceInterface interface {
CreateUser(ctx context.Context, user *User) error
GetUser(ctx context.Context, id int64) (*User, error)
UpdateUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id int64) error
ListUsers(ctx context.Context, filter Filter) ([]*User, error)
// ... ещё 20 методов
}

// ХОРОШО: маленькие интерфейсы для каждого потребителя
type UserCreator interface {
CreateUser(ctx context.Context, user *User) error
}

type UserFinder interface {
GetUser(ctx context.Context, id int64) (*User, error)
ListUsers(ctx context.Context, filter Filter) ([]*User, error)
}

6. Рекомендации по организации кода

project/
├── internal/
│ ├── service/
│ │ ├── service.go # Определяет интерфейсы для репозитория
│ │ └── service_test.go # Использует mock для тестирования
│ ├── postgres/
│ │ └── user_repo.go # Реализует интерфейс из service
│ └── handler/
│ └── handler.go # Использует service

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

Вопрос 7. Что такое пустой интерфейс (interface{}) в Go и зачем он нужен?

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

Ответ собеседника: Правильный. Пустой интерфейс позволяет хранить любой объект. До появления дженериков он был незаменим для работы с разными типами. Из него можно извлекать нужные типы через приведение типов (type assertion). Также используется при работе с неструктурированными данными, например JSON.

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

Пустой интерфейс — это интерфейс без методов. В Go 1.18 для него появился алиас any. Любой тип в Go реализует пустой интерфейс, потому что не нужно реализовывать ни одного метода.

1. Определение и базовое использование

// interface{} и any — синонимы (Go 1.18+)
var x any
var y interface{}

// Можно присвоить любое значение
x = 42
x = "hello"
x = struct{ Name string }{Name: "Go"}
x = []int{1, 2, 3}

2. Историческая роль: до дженериков

До Go 1.18 пустой интерфейс был единственным способом написать универсальный код:

// Универсальный кэш до дженериков
type Cache struct {
mu sync.RWMutex
items map[string]interface{}
}

func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}

func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}

// Использование — требует type assertion
cache.Set("user", &User{Name: "John"})
if val, ok := cache.Get("user"); ok {
user := val.(*User) // type assertion
fmt.Println(user.Name)
}

С дженериками это можно переписать безопаснее:

// Универсальный кэш с дженериками
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]V
}

func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}

// Использование — типобезопасно
userCache := Cache[string, *User]{}
userCache.Set("user1", &User{Name: "John"})
if user, ok := userCache.Get("user1"); ok {
fmt.Println(user.Name) // без type assertion
}

3. Работа с неструктурированными данными

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

// Парсинг JSON с неизвестной структурой
func parseJSON(data []byte) (map[string]any, error) {
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}

// Использование
jsonData := `{
"name": "John",
"age": 30,
"address": {
"city": "Moscow",
"zip": "123456"
},
"hobbies": ["coding", "reading"]
}`

result, _ := parseJSON([]byte(jsonData))
fmt.Println(result["name"]) // John
fmt.Println(result["address"].(map[string]any)["city"]) // Moscow

4. Type assertion и type switch

Для извлечения конкретного типа из пустого интерфейса используются type assertion и type switch:

// Type assertion — может вызвать panic
func processValue(v any) {
str := v.(string) // panic, если v не string
fmt.Println(str)
}

// Безопасный type assertion
func processValueSafe(v any) {
if str, ok := v.(string); ok {
fmt.Println("String:", str)
} else {
fmt.Println("Not a string")
}
}

// Type switch — для нескольких типов
func describe(v any) {
switch val := v.(type) {
case int:
fmt.Printf("Integer: %d\n", val)
case string:
fmt.Printf("String: %s\n", val)
case bool:
fmt.Printf("Boolean: %t\n", val)
case []any:
fmt.Printf("Slice with %d elements\n", len(val))
case map[string]any:
fmt.Printf("Map with %d keys\n", len(val))
case nil:
fmt.Println("nil")
default:
fmt.Printf("Unknown type: %T\n", val)
}
}

5. Использование в стандартной библиотеке

Многие функции стандартной библиотеки используют interface{}:

// fmt.Println принимает любое количество аргументов любого типа
func Println(a ...any) (n int, err error)

// encoding/json работает с interface{}
func Unmarshal(data []byte, v any) error

// database/sql возвращает interface{} для значений
func (r *Row) Scan(dest ...any) error

6. Паттерн: контейнер для разнородных данных

// Конфигурация с разными типами значений
type Config struct {
values map[string]any
}

func NewConfig() *Config {
return &Config{values: make(map[string]any)}
}

func (c *Config) Set(key string, value any) {
c.values[key] = value
}

func (c *Config) GetString(key string) (string, bool) {
val, ok := c.values[key]
if !ok {
return "", false
}
str, ok := val.(string)
return str, ok
}

func (c *Config) GetInt(key string) (int, bool) {
val, ok := c.values[key]
if !ok {
return 0, false
}
// JSON декодирует числа как float64
switch v := val.(type) {
case int:
return v, true
case float64:
return int(v), true
default:
return 0, false
}
}

7. Ограничения и антипаттерны

// АНТИПАТТЕРН: цепочка type assertions
func process(data any) {
// Это хрупко и трудно поддерживаемо
m := data.(map[string]any)
arr := m["items"].([]any)
first := arr[0].(map[string]any)
name := first["name"].(string)
// Любой неверный тип вызовет panic
}

// ЛУЧШЕ: использовать структуры
type Item struct {
Name string `json:"name"`
}

type Response struct {
Items []Item `json:"items"`
}

func processBetter(data []byte) error {
var resp Response
if err := json.Unmarshal(data, &resp); err != nil {
return err
}
if len(resp.Items) > 0 {
fmt.Println(resp.Items[0].Name)
}
return nil
}

8. Когда использовать пустой интерфейс

Используйте any/interface{} когда:

  • Работаете с данями неизвестной структуры (JSON, YAML)
  • Создаёте утилиты для логирования, сериализации
  • Пишете код, который должен работать с произвольными типами

Не используйте, когда:

  • Можно использовать дженерики
  • Можно определить конкретный интерфейс
  • Тип известен на этапе компиляции

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

Вопрос 8. Как устроен слайс (slice) в Go и как он ведёт себя при передаче в функции?

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

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

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

Слайс — одна из самых важных структур данных в Go. Понимание его внутреннего устройства критически важно для написания корректного и эффективного кода.

1. Внутреннее устройство слайса

Слайс — это структура с тремя полями:

// Упрощённое представление внутренней структуры слайса
type slice struct {
array unsafe.Pointer // указатель на массив в памяти
len int // текущая длина
cap int // ёмкость (сколько элементов можно вместить без реаллокации)
}

2. Создание слайсов

// Способы создания слайсов

// 1. Литерал
s1 := []int{1, 2, 3} // len=3, cap=3

// 2. make — с указанием длины и ёмкости
s2 := make([]int, 5) // len=5, cap=5
s3 := make([]int, 3, 10) // len=3, cap=10

// 3. Из массива
arr := [5]int{1, 2, 3, 4, 5}
s4 := arr[1:4] // len=3, cap=4 (от индекса 1 до конца массива)

// 4. nil слайс
var s5 []int // len=0, cap=0, array=nil

3. Поведение при передаче в функцию

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

// Изменение элементов — видно снаружи
func modifyElements(s []int) {
for i := range s {
s[i] *= 2
}
}

// Добавление элементов — НЕ видно снаружи (без указателя)
func appendElements(s []int) {
s = append(s, 4, 5, 6) // может вызвать реаллокацию
fmt.Println("Inside:", s) // [1 2 3 4 5 6]
}

// Добавление элементов — видно снаружи (через указатель)
func appendElementsPtr(s *[]int) {
*s = append(*s, 4, 5, 6)
}

func main() {
original := []int{1, 2, 3}

modifyElements(original)
fmt.Println(original) // [2 4 6] — изменения видны!

appendElements(original)
fmt.Println(original) // [2 4 6] — добавление НЕ видно

appendElementsPtr(&original)
fmt.Println(original) // [2 4 6 4 5 6] — добавление видно
}

4. Реаллокация при добавлении элементов

func demonstrateGrowth() {
s := make([]int, 0, 2)

for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("len=%d, cap=%d, ptr=%p\n", len(s), cap(s), s)
}
}

// Вывод (примерный):
// len=1, cap=2, ptr=0xc0000...
// len=2, cap=2, ptr=0xc0000...
// len=3, cap=4, ptr=0xc0001... <- реаллокация, новый указатель!
// len=4, cap=4, ptr=0xc0001...
// len=5, cap=8, ptr=0xc0002... <- ещё одна реаллокация
// ...

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

5. Разделяемая память (aliasing)

Слайсы, созданные из одного источника, разделяют память:

func demonstrateAliasing() {
original := []int{1, 2, 3, 4, 5}

// Оба слайса указывают на один массив
slice1 := original[1:3] // [2, 3]
slice2 := original[2:4] // [3, 4]

// Изменение в slice1 влияет на slice2
slice1[1] = 99

fmt.Println(original) // [1 2 99 4 5]
fmt.Println(slice1) // [2 99]
fmt.Println(slice2) // [99 4] — изменился!
}

6. Безопасное копирование слайса

// Способ 1: copy
func safeCopy1(original []int) []int {
result := make([]int, len(original))
copy(result, original)
return result
}

// Способ 2: append
func safeCopy2(original []int) []int {
return append([]int(nil), original...)
}

// Способ 3: append к пустому слайсу (Go 1.22+)
func safeCopy3(original []int) []int {
return append([]int{}, original...)
}

7. Распространённые ошибки

// ОШИБКА 1: Ожидание, что append изменит оригинал
func badAppend(s []int) {
s = append(s, 100) // это не видно вызывающему
}

// ОШИБКА 2: Утечка памяти при работе с большими слайсами
func processLargeSlice(data []int) {
// Берём маленький кусок, но весь оригинальный массив остаётся в памяти
small := data[0:3]
// data всё ещё ссылается на большой массив!

// ЛУЧШЕ: скопировать нужные данные
smallCopy := make([]int, 3)
copy(smallCopy, data[:3])
}

// ОШИБКА 3: Использование range с указателем на элемент
func badRange(s []int) {
for _, v := range s {
v = v * 2 // изменяет копию, не оригинал!
}

// ПРАВИЛЬНО:
for i := range s {
s[i] = s[i] * 2
}
}

8. Практические рекомендации

  • Если функция добавляет элементы — возвращайте новый слайс или используйте указатель.
  • Если функция только читает — передавайте слайс по значению.
  • Для защиты от изменений — копируйте слайс.
  • При работе с большими данными — следите за утечками памяти через slicing.
// Хорошая практика: функция возвращает новый слайс
func filterPositive(nums []int) []int {
result := make([]int, 0, len(nums))
for _, n := range nums {
if n > 0 {
result = append(result, n)
}
}
return result
}

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

Вопрос 9. Что такое map в Go, как она устроена и какая сложность поиска?

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

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

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

Map в Go — это встроенная хеш-таблица, которая обеспечивает быстрый доступ по ключу. Понимание её внутреннего устройства помогает писать более эффективный код.

1. Внутреннее устройство map

Map в Go реализована как хеш-таблица с цепочками (separate chaining) для разрешения коллизий:

// Упрощённое представление внутренней структуры
type hmap struct {
count int // количество элементов
flags uint8
B uint8 // лог2 количества бакетов (2^B бакетов)
noverflow uint16
hash0 uint32 // seed для хеш-функции
buckets unsafe.Pointer // массив бакетов
oldbuckets unsafe.Pointer // предыдущий массив (при росте)
nevacuate uintptr
extra *mapextra
}

// Бакет — структура, хранящая пары ключ-значение
type bmap struct {
tophash [8]uint8 // старшие биты хеша для быстрого сравнения
// За ними следуют ключи и значения (по 8 пар)
}

2. Создание и использование map

// Способы создания map

// 1. Литерал
m1 := map[string]int{
"one": 1,
"two": 2,
}

// 2. make — с указанием начальной ёмкости
m2 := make(map[string]int) // начальная ёмкость по умолчанию
m3 := make(map[string]int, 1000) // начальная ёмкость ~1000 элементов

// 3. nil map — только для чтения!
var m4 map[string]int // nil map
// m4["key"] = 1 // panic: assignment to entry in nil map

// Операции
m1["three"] = 3 // вставка
val := m1["one"] // чтение
delete(m1, "two") // удаление

// Проверка существования ключа
if val, ok := m1["one"]; ok {
fmt.Println("Found:", val)
}

// Итерация (порядок не гарантирован!)
for key, val := range m1 {
fmt.Println(key, val)
}

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

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

Худший случай O(n) возникает при большом количестве коллизий, когда все ключи попадают в один бакет. Go использует рандомизированную хеш-функцию, что делает атаки на коллизии практически невозможными.

4. Рост map

Map автоматически растёт, когда загрузка превышает порог (load factor ≈ 6.5):

func demonstrateGrowth() {
m := make(map[int]int)

for i := 0; i < 100000; i++ {
m[i] = i
if i % 10000 == 0 {
fmt.Printf("Elements: %d\n", len(m))
}
}
}

При росте Go создаёт новый массив бакетов в 2 раза больше и постепенно переносит элементы (incremental evacuation), чтобы не блокировать операции.

5. Типы ключей

Map может использовать в качестве ключа любой сравнимый тип:

// Допустимые типы ключей:
// - Все примитивные типы (int, string, bool, float64)
// - Указатели
// - Структуры с сравнимыми полями
// - Массивы (не слайсы!)

type Point struct {
X, Y int
}

// Структура как ключ
locations := map[Point]string{
{0, 0}: "origin",
{1, 0}: "east",
}

// Слайс НЕ может быть ключом (не сравним)
// bad := map[[]int]string{} // ошибка компиляции

6. Потокобезопасность

Map в Go НЕ потокобезопасна:

// ОПАСНО: конкурентный доступ к map
func unsafeConcurrent() {
m := make(map[int]int)

// Горутина 1: запись
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
}
}()

// Горутина 2: чтение
go func() {
for i := 0; i < 1000; i++ {
_ = m[i]
}
}()
// fatal error: concurrent map read and map write
}

7. Потокобезопасные альтернативы

// Вариант 1: sync.Mutex
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}

func (s *SafeMap) Get(key string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.m[key]
return val, ok
}

func (s *SafeMap) Set(key string, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}

// Вариант 2: sync.Map (для специфических сценариев)
func syncMapExample() {
var m sync.Map

// Хранение и загрузка
m.Store("key", "value")
if val, ok := m.Load("key"); ok {
fmt.Println(val)
}

// Атомарная операция: загрузить или сохранить
actual, loaded := m.LoadOrStore("key2", "value2")

// Удаление
m.Delete("key")

// Итерация
m.Range(func(key, value any) bool {
fmt.Println(key, value)
return true // продолжить итерацию
})
}

8. Особенности sync.Map

sync.Map оптимизирован для двух сценариев:

  • Ключи, которые записываются один раз, но читаются много раз (cache)
  • Множество горутин, работающих с непересекающимися наборами ключей
// Когда использовать sync.Map:
// - Кэши с редкой записью и частым чтением
// - Регистры, где каждый ключ пишется один раз

// Когда НЕ использовать sync.Map:
// - Частые записи и чтения одних и тех же ключей
// - Нужен детерминированный порядок итерации

9. Практические рекомендации

// 1. Указывайте начальную ёмкость, если знаете размер
// ПЛОХО
m := make(map[string]int)
for i := 0; i < 1000000; i++ {
m[strconv.Itoa(i)] = i // множественные реаллокации
}

// ХОРОШО
m := make(map[string]int, 1000000)
for i := 0; i < 1000000; i++ {
m[strconv.Itoa(i)] = i
}

// 2. Используйте comma ok идиому для проверки существования
if val, ok := m["key"]; ok {
// ключ существует
} else {
// ключ не существует
}

// 3. Не полагайтесь на порядок итерации
for k, v := range m {
// порядок каждый раз разный!
}

// 4. Для подсчёта элементов используйте len()
count := len(m)

Итог: Map в Go — это хеш-таблица со средней сложностью O(1) для основных операций. Она автоматически растёт, но не потокобезопасна. Для конкурентного доступа используйте sync.RWMutex или sync.Map (для специфических сценариев). Указывайте начальную ёмкость, если знаете примерный размер.

Вопрос 10. Насколько безопасна map для конкурентного доступа?

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

Ответ собеседника: Правильный. Map не безопасна для конкурентной записи. Конкурентное чтение возможно, но запись требует синхронизации. Для безопасной работы можно использовать мьютекс или sync.Map из стандартной библиотеки. В команде предпочитают обычную map с мьютексом, так как sync.Map имеет ограничения и сложнее в использовании.

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

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

1. Что происходит при конкурентном доступе

// Это вызовет панику!
func concurrentWrite() {
m := make(map[string]int)

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m["key"] = i // fatal error: concurrent map writes
}(i)
}
wg.Wait()
}

Go runtime детектирует конкурентную запись и вызывает fatal error: concurrent map writes. Это не паника, которую можно перехватить — это аварийное завершение программы.

2. Комбинации конкурентного доступа

СценарийБезопасно?
Только чтение (множество горутин)Да
Одна горутина пишет, остальные читаютНет
Несколько горутин пишутНет
Одна горутина пишет, одна читаетНет

3. Решение 1: sync.RWMutex с обычной map

Это наиболее распространённый и рекомендуемый подход:

type SafeMap[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}

func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{
m: make(map[K]V),
}
}

func (s *SafeMap[K, V]) Get(key K) (V, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.m[key]
return val, ok
}

func (s *SafeMap[K, V]) Set(key K, value V) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}

func (s *SafeMap[K, V]) Delete(key K) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.m, key)
}

func (s *SafeMap[K, V]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.m)
}

// Атомарная операция: получить или установить
func (s *SafeMap[K, V]) GetOrSet(key K, value V) (V, bool) {
s.mu.Lock()
defer s.mu.Unlock()

if val, ok := s.m[key]; ok {
return val, true
}
s.m[key] = value
return value, false
}

// Использование
func example() {
cache := NewSafeMap[string, *User]()

// Конкурентная запись
go cache.Set("user1", &User{Name: "John"})

// Конкурентное чтение
if user, ok := cache.Get("user1"); ok {
fmt.Println(user.Name)
}
}

4. Решение 2: sync.Map

sync.Map — это специализированная реализация, оптимизированная для конкретных сценариев:

func syncMapExample() {
var m sync.Map

// Store — запись
m.Store("key1", "value1")

// Load — чтение
if val, ok := m.Load("key1"); ok {
fmt.Println(val)
}

// LoadOrStore — атомарная операция
actual, loaded := m.LoadOrStore("key2", "value2")
if loaded {
fmt.Println("Key already exists:", actual)
}

// LoadAndDelete — атомарное чтение и удаление
if val, ok := m.LoadAndDelete("key1"); ok {
fmt.Println("Deleted:", val)
}

// Range — итерация (может не включать новые записи)
m.Range(func(key, value any) bool {
fmt.Println(key, value)
return true // продолжить
})

// Delete — удаление
m.Delete("key1")
}

5. Сравнение подходов

Аспектmap + RWMutexsync.Map
ТипобезопасностьДа (дженерики)Нет (any)
Производительность (частые чтения)ХорошаяОтличная
Производительность (частые записи)ХорошаяХуже
Атомарные операцииНужно реализовыватьВстроены
ИтерацияПростаяСложная
Подсчёт элементовlen()Нужно итерировать

6. Бенчмарки

func BenchmarkMapWithRWMutex(b *testing.B) {
m := NewSafeMap[int, int]()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
m.Set(i, i)
m.Get(i)
i++
}
})
}

func BenchmarkSyncMap(b *testing.B) {
var m sync.Map
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
m.Store(i, i)
m.Load(i)
i++
}
})
}

Результаты зависят от паттерна доступа, но в целом:

  • При частых чтениях и редких записях: sync.Map быстрее
  • При смешанном доступе: map + RWMutex обычно лучше

7. Паттерн: sharded map для высокой нагрузки

Для снижения конкуренции на мьютекс можно использовать шардирование:

type ShardedMap[K comparable, V any] struct {
shards []*SafeMap[K, V]
shardCount uint32
}

func NewShardedMap[K comparable, V any](shardCount int) *ShardedMap[K, V] {
shards := make([]*SafeMap[K, V], shardCount)
for i := 0; i < shardCount; i++ {
shards[i] = NewSafeMap[K, V]()
}
return &ShardedMap[K, V]{
shards: shards,
shardCount: uint32(shardCount),
}
}

func (s *ShardedMap[K, V]) getShard(key K) *SafeMap[K, V] {
// Простая хеш-функция для определения шарда
h := fnv.New32a()
fmt.Fprintf(h, "%v", key)
return s.shards[h.Sum32()%s.shardCount]
}

func (s *ShardedMap[K, V]) Get(key K) (V, bool) {
return s.getShard(key).Get(key)
}

func (s *ShardedMap[K, V]) Set(key K, value V) {
s.getShard(key).Set(key, value)
}

8. Рекомендации

  • По умолчанию используйте map + sync.RWMutex — это предсказуемо и типобезопасно.
  • sync.Map — только для кэшей с паттерном «запись один раз, чтение много раз».
  • Sharded map — для высоконагруженных систем с тысячами горутин.
  • Никогда не используйте обычную map без синхронизации в конкурентном коде.

Итог: Map в Go не безопасна для конкурентного доступа. Для безопасной работы используйте sync.RWMutex (рекомендуется в большинстве случаев) или sync.Map (для специфических сценариев с частым чтением).

Вопрос 11. В чём разница между объявлением переменных через var и := в контексте map и slice?

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

Ответ собеседника: Правильный. При объявлении через var переменная создаётся без инициализации — для map это nil-мапа, в которую нельзя записать данные (будет паника). Для slice через var создаётся nil-слайс, в который можно добавлять элементы через append. Через := с make структура инициализируется и готова к использованию сразу.

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

Понимание разницы между var и := критически важно для корректной работы с коллекциями в Go. Особенно это касается map, где использование nil-значения приводит к панике.

1. Объявление через var — zero values

При объявлении через var переменная получает zero value для своего типа:

// Map
var m map[string]int
// m == nil
// len(m) == 0
// Чтение из nil map возвращает zero value
val := m["key"] // val == 0, без паники
// Запись в nil map — паника!
// m["key"] = 1 // panic: assignment to entry in nil map

// Slice
var s []int
// s == nil
// len(s) == 0
// cap(s) == 0
// append к nil слайсу работает корректно
s = append(s, 1) // [1] — создаётся новый слайс

// Array — не путать со слайсом!
var a [5]int
// a == [0, 0, 0, 0, 0] — массив всегда инициализирован

2. Объявление через := с make

// Map — инициализирована и готова к использованию
m := make(map[string]int)
m["key"] = 1 // OK

// Map с начальной ёмкостью
m2 := make(map[string]int, 100)

// Slice — инициализирован и готов
s := make([]int, 0) // len=0, cap=0
s2 := make([]int, 5) // len=5, cap=5, заполнен нулями
s3 := make([]int, 0, 10) // len=0, cap=10

3. Объявление через := с литералом

// Map литерал
m := map[string]int{
"one": 1,
"two": 2,
}

// Пустой map литерал — НЕ nil!
m2 := map[string]int{}
m2["key"] = 1 // OK — это не nil map

// Slice литерал
s := []int{1, 2, 3}

// Пустой slice литерал — НЕ nil!
s2 := []int{}
fmt.Println(s2 == nil) // false

4. Ключевое отличие nil map и пустой map

func demonstrateDifference() {
// Nil map
var nilMap map[string]int
fmt.Println(nilMap == nil) // true

// Чтение из nil map — безопасно
val := nilMap["key"] // val == 0

// Запись в nil map — паника
// nilMap["key"] = 1 // panic!

// Пустая map
emptyMap := map[string]int{}
fmt.Println(emptyMap == nil) // false

// Запись в пустую map — безопасно
emptyMap["key"] = 1 // OK

// Проверка на существование ключа
if val, ok := nilMap["key"]; ok {
fmt.Println("Found:", val) // не выполнится
}
}

5. Ключевое отличие nil slice и пустого slice

func demonstrateSliceDifference() {
// Nil slice
var nilSlice []int
fmt.Println(nilSlice == nil) // true
fmt.Println(len(nilSlice)) // 0
fmt.Println(cap(nilSlice)) // 0

// append к nil slice работает
nilSlice = append(nilSlice, 1)
fmt.Println(nilSlice) // [1]

// Пустой slice
emptySlice := []int{}
fmt.Println(emptySlice == nil) // false
fmt.Println(len(emptySlice)) // 0

// Итерация по обоим — безопасна
for _, v := range nilSlice {
fmt.Println(v) // 1
}
for _, v := range emptySlice {
fmt.Println(v) // не выполнится
}

// JSON сериализация — разная!
nilJSON, _ := json.Marshal(nilSlice) // null
emptyJSON, _ := json.Marshal(emptySlice) // []
fmt.Println(string(nilJSON)) // null
fmt.Println(string(emptyJSON)) // []
}

6. Практические последствия

// Проблема: функция возвращает nil slice
func findItems(condition bool) []int {
if !condition {
return nil // или просто return
}
return []int{1, 2, 3}
}

// Проверка результата
func process(condition bool) {
items := findItems(condition)

// ПЛОХО: проверка на nil не всегда корректна
if items != nil {
// А если функция вернула []int{}?
}

// ХОРОШО: проверка длины
if len(items) > 0 {
// Обработка
}
}

7. Когда использовать nil, а когда пустую коллекцию

// Используйте nil slice/map, когда:
// 1. Нужно отличить "не инициализировано" от "пусто"
// 2. Экономия памяти (nil не выделяет память)
// 3. Возвращение из функции при ошибке

func getUsers(filter string) ([]User, error) {
if filter == "" {
return nil, errors.New("filter is required")
}
// ...
return users, nil
}

// Используйте пустую коллекцию, когда:
// 1. Нужно гарантировать не-nil значение
// 2. Работа с JSON (null vs [])
// 3. Итерация без проверки на nil

func getItems() []Item {
// Возвращаем пустой slice вместо nil
// чтобы вызывающий код не проверял на nil
return []Item{}
}

8. Таблица сравнения

Аспектvar m map[K]Vm := make(map[K]V)m := map[K]V{}
nil?ДаНетНет
ЗаписьПаникаOKOK
ЧтениеZero valueZero valueZero value
len()000
JSONnull{}{}
Аспектvar s []Ts := make([]T, 0)s := []T{}
nil?ДаНетНет
appendРаботаетРаботаетРаботает
len()000
JSONnull[][]

Итог: var создаёт nil-значения, которые ведут себя по-разному для map и slice. Map требует инициализации через make или литерал перед записью. Slice можно использовать с append даже будучи nil. Для JSON и API лучше возвращать пустые коллекции вместо nil.

Вопрос 12. Какие типы мьютексов из пакета sync используете и чем RWMutex отличается от обычного Mutex?

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

Ответ собеседника: Правильный. Использую Mutex и RWMutex. RWMutex разделяет доступ на чтение и запись — много горутин могут одновременно читать, но запись эксклюзивна. Когда приходит писатель, он блокирует и читателей, и других писателей. Это удобно при частых чтениях и редких записях.

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

Мьютексы — это фундаментальный инструмент синхронизации в Go. Понимание различий между Mutex и RWMutex помогает выбрать правильный инструмент для каждой задачи.

1. sync.Mutex — эксклюзивная блокировка

Mutex предоставляет простую эксклюзивную блокировку: только одна горутина может владеть блокировкой в каждый момент времени.

type Counter struct {
mu sync.Mutex
value int
}

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

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

// Использование
func main() {
counter := &Counter{}

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.Value()) // 1000
}

2. sync.RWMutex — разделение на чтение и запись

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

type Cache struct {
mu sync.RWMutex
data map[string]*User
}

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

// Чтение — множество горутин могут читать одновременно
func (c *Cache) Get(id string) (*User, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
user, ok := c.data[id]
return user, ok
}

// Запись — эксклюзивный доступ
func (c *Cache) Set(id string, user *User) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[id] = user
}

// Запись — эксклюзивный доступ
func (c *Cache) Delete(id string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, id)
}

3. Внутреннее устройство RWMutex

// Упрощённое представление
type RWMutex struct {
w sync.Mutex // для писателей
writerSem uint32 // семафор для писателей
readerSem uint32 // семафор для читателей
readerCount int32 // количество активных читателей
readerWait int32 // количество читателей, ожидающих писателя
}

Правила работы RWMutex:

  • Множество горутин могут одновременно удерживать RLock()
  • Lock() ждёт, пока все читатели не отпустят RLock()
  • Если есть писатель, ожидающий Lock(), новые читатели блокируются
  • Это предвращает «голодание» писателей

4. Сравнение производительности

func BenchmarkMutexRead(b *testing.B) {
var mu sync.Mutex
m := make(map[string]int)
m["key"] = 42

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
_ = m["key"]
mu.Unlock()
}
})
}

func BenchmarkRWMutexRead(b *testing.B) {
var mu sync.RWMutex
m := make(map[string]int)
m["key"] = 42

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.RLock()
_ = m["key"]
mu.RUnlock()
}
})
}

Типичные результаты:

  • Только чтение: RWMutex быстрее в 2-10 раз
  • Смешанный доступ: зависит от соотношения чтения/записи
  • Только запись: Mutex может быть быстрее

5. Когда использовать каждый тип

Используйте Mutex, когда:

  • Простая синхронизация без частых чтений
  • Операции чтения и записи перемешаны
  • Нужна минимальная накладная стоимость на запись
// Пример: счётчик с частыми записями
type Metrics struct {
mu sync.Mutex
counters map[string]int64
}

func (m *Metrics) Add(name string, value int64) {
m.mu.Lock()
defer m.mu.Unlock()
m.counters[name] += value
}

Используйте RWMutex, когда:

  • Частые чтения и редкие записи
  • Кэши и реестры
  • Конфигурация, которая читается при каждом запросе
// Пример: кэш с частыми чтениями
type UserCache struct {
mu sync.RWMutex
users map[int64]*User
ttl time.Duration
}

func (c *UserCache) Get(id int64) (*User, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
user, ok := c.users[id]
return user, ok
}

func (c *UserCache) Refresh(users map[int64]*User) {
c.mu.Lock()
defer c.mu.Unlock()
c.users = users
}

6. Распространённые ошибки

// ОШИБКА 1: Забыли разблокировать
func badUnlock(mu *sync.Mutex) {
mu.Lock()
if someCondition {
return // Забыли Unlock!
}
mu.Unlock()
}

// ПРАВИЛЬНО: всегда используйте defer
func goodUnlock(mu *sync.Mutex) {
mu.Lock()
defer mu.Unlock()
if someCondition {
return // Unlock вызовется автоматически
}
}

// ОШИБКА 2: Рекурсивная блокировка
func recursiveLock(mu *sync.Mutex) {
mu.Lock()
defer mu.Unlock()
recursiveLock(mu) // deadlock! Go не поддерживает рекурсивные мьютексы
}

// ОШИБКА 3: Копирование мьютекса
type BadStruct struct {
mu sync.Mutex
data int
}

func copyProblem() {
original := BadStruct{data: 42}
copy := original // Копирует мьютекс — неопределённое поведение!
copy.mu.Lock() // Это другой мьютекс
}

// ПРАВИЛЬНО: используйте указатель
func pointerSolution() {
original := &BadStruct{data: 42}
copy := original // Копирует указатель — мьютекс общий
copy.mu.Lock() // OK
}

// ОШИБКА 4: Использование мьютекса по значению в методах
type BadService struct {
mu sync.Mutex
}

func (s BadService) Process() { // Приёмник по значению!
s.mu.Lock() // Блокировка копии мьютекса
defer s.mu.Unlock()
// Другие горутины тоже заблокируют свою копию — нет синхронизации!
}

// ПРАВИЛЬНО: приёмник по указателю
type GoodService struct {
mu sync.Mutex
}

func (s *GoodService) Process() { // Приёмник по указателю
s.mu.Lock() // Блокировка общего мьютекса
defer s.mu.Unlock()
}

7. Паттерн: обёртка для типобезопасности

// Дженерик-обёртка для любого типа
type Protected[T any] struct {
mu sync.RWMutex
value T
}

func NewProtected[T any](value T) *Protected[T] {
return &Protected[T]{value: value}
}

func (p *Protected[T]) Get() T {
p.mu.RLock()
defer p.mu.RUnlock()
return p.value
}

func (p *Protected[T]) Set(value T) {
p.mu.Lock()
defer p.mu.Unlock()
p.value = value
}

// Выполнение функции с эксклюзивным доступом
func (p *Protected[T]) Update(fn func(T) T) {
p.mu.Lock()
defer p.mu.Unlock()
p.value = fn(p.value)
}

// Использование
func example() {
counter := NewProtected(0)

// Чтение
fmt.Println(counter.Get())

// Запись
counter.Set(42)

// Атомарное обновление
counter.Update(func(v int) int {
return v + 1
})
}

8. Другие инструменты синхронизации из sync

// sync.Once — выполнение ровно один раз
var once sync.Once
var instance *Database

func GetDatabase() *Database {
once.Do(func() {
instance = connectToDatabase()
})
return instance
}

// sync.WaitGroup — ожидание завершения горутин
func processItems(items []Item) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
process(i)
}(item)
}
wg.Wait()
}

// sync.Cond — сигнализирование между горутинами
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}

func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}

func (q *Queue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // Уведомить одного ожидающего
}

func (q *Queue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // Ждать, пока не появится элемент
}
item := q.items[0]
q.items = q.items[1:]
return item
}

Итог: Mutex — для простой эксклюзивной блокировки, RWMutex — для сценариев с частыми чтениями и редкими записями. Всегда используйте defer для разблокировки, не копируйте мьютексы, используйте приёмники по указателю.

Вопрос 13. Как работают каналы в Go и насколько они безопасны для конкурентного доступа?

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

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

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

Каналы — это одна из ключевых особенностей Go, реализующая принцип CSP (Communicating Sequential Processes). Они позволяют горутинам безопасно обмениваться данными без явной синхронизации.

1. Внутреннее устройство канала

// Упрощённое представление внутренней структуры
type hchan struct {
qcount uint // количество элементов в очереди
dataqsiz uint // размер буфера
buf unsafe.Pointer // кольцевой буфер
sendx uint // индекс для отправки
recvx uint // индекс для приёма
recvq waitq // очередь ожидающих получателей
sendq waitq // очередь ожидающих отправителей
lock mutex // мьютекс для защиты структуры
}

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

2. Типы каналов

// Небуферизованный канал — блокирует до готовности получателя
ch1 := make(chan int)

// Буферизованный канал — блокирует только когда буфер полон
ch2 := make(chan int, 10)

// Канал только для чтения
func reader(ch <-chan int) {
for val := range ch {
fmt.Println(val)
}
}

// Канал только для записи
func writer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}

3. Базовые операции

func basicOperations() {
ch := make(chan int, 3)

// Отправка
ch <- 1
ch <- 2
ch <- 3

// Получение
val := <-ch // 1

// Проверка на закрытие
val, ok := <-ch // 2, true

// Закрытие
close(ch)

// Чтение из закрытого канала
val, ok = <-ch // 3, true
val, ok = <-ch // 0, false — канал закрыт

// Итерация по каналу
for val := range ch {
fmt.Println(val)
}
}

4. Паттерн: worker pool

func workerPool() {
const numWorkers = 5
const numJobs = 20

jobs := make(chan int, numJobs)
results := make(chan int, numJobs)

// Запуск воркеров
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
result := job * 2
results <- result
}
}(w)
}

// Отправка задач
go func() {
for j := 0; j < numJobs; j++ {
jobs <- j
}
close(jobs)
}()

// Ожидание завершения и закрытие results
go func() {
wg.Wait()
close(results)
}()

// Сбор результатов
for result := range results {
fmt.Println(result)
}
}

5. Паттерн: select с таймаутом

func withTimeout() {
ch := make(chan int)

go func() {
time.Sleep(2 * time.Second)
ch <- 42
}()

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}

6. Паттерн: graceful shutdown

func gracefulShutdown() {
done := make(chan struct{})

// Обработка сигналов
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// Рабочий процесс
go func() {
for {
select {
case <-done:
fmt.Println("Shutting down...")
return
default:
// Работа
time.Sleep(100 * time.Millisecond)
}
}
}()

// Ожидание сигнала
<-sigChan
close(done) // Сигнал к завершению
time.Sleep(500 * time.Millisecond) // Даём время на завершение
}

7. Паттерн: fan-out / fan-in

// Fan-out: распределение работы между воркерами
func fanOut(input <-chan int, n int) []<-chan int {
channels := make([]<-chan int, n)
for i := 0; i < n; i++ {
channels[i] = process(input)
}
return channels
}

// Fan-in: объединение результатов
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
multiplexed := make(chan int)

multiplex := func(c <-chan int) {
defer wg.Done()
for val := range c {
multiplexed <- val
}
}

wg.Add(len(channels))
for _, c := range channels {
go multiplex(c)
}

go func() {
wg.Wait()
close(multiplexed)
}()

return multiplexed
}

func process(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for val := range input {
output <- val * 2
}
}()
return output
}

8. Распространённые ошибки

// ОШИБКА 1: Отправка в закрытый канал — panic
func sendToClosed() {
ch := make(chan int, 1)
close(ch)
// ch <- 1 // panic: send on closed channel
}

// ОШИБКА 2: Закрытие канала из получателя
func closeFromReceiver() {
ch := make(chan int, 1)
go func() {
// close(ch) // Ошибка! Закрывать должен отправитель
}()
}

// ОШИБКА 3: Утечка горутины из-за заблокированного канала
func goroutineLeak() {
ch := make(chan int)

go func() {
// Эта горутина заблокируется навсегда, если никто не прочитает
ch <- 42
}()

// Горутина утечёт, если мы не прочитаем из канала
}

// ПРАВИЛЬНО: используйте буферизованный канал или context
func noLeak() {
ch := make(chan int, 1) // Буферизованный

go func() {
ch <- 42 // Не заблокируется
}()

// Можно прочитать позже или вообще не читать
}

// ОШИБКА 4: Двойное закрытие канала
func doubleClose() {
ch := make(chan int)
close(ch)
// close(ch) // panic: close of closed channel
}

// ПРАВИЛЬНО: sync.Once для безопасного закрытия
func safeClose() {
ch := make(chan int)
var once sync.Once

closeOnce := func() {
once.Do(func() {
close(ch)
})
}

go closeOnce()
go closeOnce() // Безопасно, закроется только один раз
}

9. Производительность: каналы vs мьютексы

// Бенчмарк: канал vs мьютекс для передачи данных
func BenchmarkChannel(b *testing.B) {
ch := make(chan int, 1)
go func() {
for range ch {}
}()

b.ResetTimer()
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
}

func BenchmarkMutex(b *testing.B) {
var mu sync.Mutex
var val int

b.ResetTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
val = i
mu.Unlock()
}
}

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

10. Когда использовать каналы, а когда мьютексы

Используйте каналы, когда:

  • Нужно передавать данные между горутинами
  • Нужна координация работы (fan-out, fan-in, pipeline)
  • Нужны таймауты и отмена через select
  • Реализуете паттерны producer-consumer

Используйте мьютексы, когда:

  • Нужна простая защита общего состояния
  • Высокая частота операций (счетчики, кэши)
  • Нет необходимости в передаче данных между горутинами

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

Вопрос 14. Как правильно закрыть канал, если есть несколько писателей, и что будет при записи в закрытый канал?

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

Ответ собеседника: Правильный. Закрывать канал должен тот, кто его создал, обычно на уровне выше. Нельзя закрыть канал дважды — будет паника. Запись в закрытый канал вызывает панику. Чтение из закрытого канала возвращает нулевое значение и второй параметр false, что позволяет отличить закрытие от отправки нулевого значения.

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

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

1. Запись в закрытый канал — panic

func writeToClosed() {
ch := make(chan int, 1)
close(ch)

// Это вызовет panic!
// ch <- 42 // panic: send on closed channel

// Чтение из закрытого канала — безопасно
val, ok := <-ch
fmt.Println(val, ok) // 0, false

// Если в канале есть данные, сначала прочитаются они
ch2 := make(chan int, 2)
ch2 <- 1
ch2 <- 2
close(ch2)

fmt.Println(<-ch2) // 1
fmt.Println(<-ch2) // 2
val, ok = <-ch2 // 0, false — канал закрыт и пуст
}

2. Двойное закрытие — panic

func doubleClose() {
ch := make(chan int)
close(ch)
// close(ch) // panic: close of closed channel
}

3. Решение для нескольких писателей: sync.Once

type MultiWriterChannel struct {
ch chan int
once sync.Once
}

func NewMultiWriterChannel() *MultiWriterChannel {
return &MultiWriterChannel{
ch: make(chan int, 100),
}
}

func (m *MultiWriterChannel) Close() {
m.once.Do(func() {
close(m.ch)
})
}

func (m *MultiWriterChannel) Send(val int) {
m.ch <- val
}

func (m *MultiWriterChannel) Receive() <-chan int {
return m.ch
}

// Использование
func example() {
ch := NewMultiWriterChannel()

// Несколько писателей
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 10; j++ {
ch.Send(id*10 + j)
}
ch.Close() // Безопасно вызывать из нескольких горутин
}(i)
}

// Чтение
for val := range ch.Receive() {
fmt.Println(val)
}
}

4. Решение: WaitGroup для координации закрытия

func coordinatedClose() {
ch := make(chan int, 100)
var wg sync.WaitGroup

// Запуск писателей
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
ch <- id*10 + j
}
}(i)
}

// Закрытие канала после завершения всех писателей
go func() {
wg.Wait()
close(ch)
}()

// Чтение
for val := range ch {
fmt.Println(val)
}
}

5. Решение: отдельный канал для сигнала завершения

func doneChannel() {
ch := make(chan int, 100)
done := make(chan struct{})

// Писатели
for i := 0; i < 5; i++ {
go func(id int) {
defer func() {
// Сигнализируем о завершении
}()
for j := 0; j < 10; j++ {
select {
case ch <- id*10 + j:
case <-done:
return // Прерывание по сигналу
}
}
}(i)
}

// Счётчик завершённых писателей
var completed int
for val := range ch {
fmt.Println(val)
completed++
if completed >= 50 {
close(done) // Сигнал к завершению
break
}
}
}

6. Решение: context с отменой

func contextCancel() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

ch := make(chan int, 100)

// Писатели
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 100; j++ {
select {
case ch <- id*100 + j:
case <-ctx.Done():
return
}
}
}(i)
}

// Чтение с возможностью отмены
go func() {
for val := range ch {
fmt.Println(val)
}
}()

// Отмена через некоторое время
time.Sleep(100 * time.Millisecond)
cancel()
}

7. Паттерn: владелец канала

Лучшая практика — чёткое разделение ответственности:

// Владелец канала отвечает за его закрытие
type ChannelOwner struct {
ch chan int
closed atomic.Bool
}

func NewChannelOwner() *ChannelOwner {
return &ChannelOwner{
ch: make(chan int, 100),
}
}

func (o *ChannelOwner) Send(val int) error {
if o.closed.Load() {
return errors.New("channel is closed")
}
o.ch <- val
return nil
}

func (o *ChannelOwner) Close() {
if o.closed.CompareAndSwap(false, true) {
close(o.ch)
}
}

func (o *ChannelOwner) Channel() <-chan int {
return o.ch
}

// Использование
func ownerExample() {
owner := NewChannelOwner()

// Писатели
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 10; j++ {
if err := owner.Send(id*10 + j); err != nil {
return // Канал закрыт
}
}
}(i)
}

// Закрытие после завершения
go func() {
time.Sleep(time.Second)
owner.Close()
}()

// Чтение
for val := range owner.Channel() {
fmt.Println(val)
}
}

8. Проверка закрытия канала без чтения

// Невозможно проверить, закрыт ли канал, без чтения
// Но можно использовать select с default
func isClosed(ch chan int) bool {
select {
case <-ch:
return true // Канал закрыт (или есть данные)
default:
return false // Канал открыт и пуст
}
}

// Для проверки закрытия используйте comma ok
func checkClosed(ch chan int) {
val, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
} else {
fmt.Println("Received:", val)
}
}

9. Таблица поведения каналов

ОперацияОткрытый каналЗакрытый каналnil канал
ОтправкаБлокирует/записываетpanicБлокирует навсегда
ПолучениеБлокирует/читаетВозвращает zero valueБлокирует навсегда
ЗакрытиеOKpanicpanic
len/capРаботаетРаботает0

10. Рекомендации

  • Закрывайте канал только из отправителя
  • Используйте sync.Once для безопасного закрытия из нескольких горутин
  • Используйте WaitGroup для координации закрытия
  • Используйте context для отмены
  • Не закрывайте канал из получателя
  • Проверяйте закрытие через comma ok: val, ok := <-ch

Итог: Запись в закрытый канал и двойное закрытие вызывают panic. Для безопасного закрытия из нескольких писателей используйте sync.Once, WaitGroup или context. Лучшая практика — чёткое разделение: владелец канала отвечает за его закрытие.

Вопрос 15. Как работает select с default и какие проблемы могут возникнуть при использовании таймера в select?

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

Ответ собеседника: Правильный. Default выполняется, если ни один из каналов не готов — это неблокирующая проверка. Проблема с таймером: select недетерминирован, и при частых тиках таймер может сработать раньше, чем придёт сообщение из канала. Это особенно заметно на маленьких интервалах. Для надёжности лучше использовать контекст с тайм-аутом, который можно передавать в другие функции и проверять отмену.

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

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

1. Базовый select

func basicSelect() {
ch1 := make(chan int)
ch2 := make(chan int)

go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

// Блокируется до готовности любого канала
select {
case val := <-ch1:
fmt.Println("ch1:", val)
case val := <-ch2:
fmt.Println("ch2:", val)
}
}

2. Select с default — неблокирующее поведение

func nonBlockingSelect() {
ch := make(chan int, 1)

// Неблокирующее чтение
select {
case val := <-ch:
fmt.Println("Received:", val)
default:
fmt.Println("No data available")
}

// Неблокирующая запись
select {
case ch <- 42:
fmt.Println("Sent")
default:
fmt.Println("Channel is full")
}
}

3. Проблема недетерминизма select

Когда несколько каналов готовы одновременно, Go выбирает случайный:

func nonDeterminism() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)

ch1 <- 1
ch2 <- 2

// Какой канал будет выбран — непредсказуемо!
select {
case val := <-ch1:
fmt.Println("ch1:", val) // 50% вероятность
case val := <-ch2:
fmt.Println("ch2:", val) // 50% вероятность
}
}

4. Проблема с таймером в select

func timerProblem() {
ch := make(chan int)

go func() {
time.Sleep(50 * time.Millisecond)
ch <- 42
}()

// ПРОБЛЕМА: Таймер может сработать раньше, чем данные
select {
case val := <-ch:
fmt.Println("Received:", val)
case <-time.After(100 * time.Millisecond):
fmt.Println("Timeout!")
}
}

Проблема time.After в том, что он создаёт новый таймер при каждом выполнении select, и эти таймеры не собираются GC до их срабатывания.

5. Проблема утечки таймеров

func timerLeak() {
ch := make(chan int)

// ПЛОХО: При каждом вызове создаётся новый таймер
for i := 0; i < 1000000; i++ {
select {
case val := <-ch:
fmt.Println(val)
case <-time.After(5 * time.Second):
// Если данные приходят быстро, таймеры накапливаются
// и не собираются GC до истечения 5 секунд
fmt.Println("Timeout")
}
}
}

6. Решение: используйте time.NewTimer

func properTimer() {
ch := make(chan int)
timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // Важно! Освобождает ресурсы

select {
case val := <-ch:
if !timer.Stop() {
<-timer.C // Очистить канал таймера
}
fmt.Println("Received:", val)
case <-timer.C:
fmt.Println("Timeout")
}
}

7. Решение: context с таймаутом

func contextTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

ch := make(chan int)

go func() {
// Имитация долгой работы
time.Sleep(10 * time.Second)
select {
case ch <- 42:
case <-ctx.Done():
// Контекст отменён, не отправляем
return
}
}()

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-ctx.Done():
fmt.Println("Timeout or cancelled:", ctx.Err())
}
}

8. Паттерн: циклический select с таймером

func cyclicSelect() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

timeout := time.After(10 * time.Second)

for {
select {
case <-ticker.C:
fmt.Println("Tick")
case <-timeout:
fmt.Println("Done")
return
}
}
}

9. Паттерн: приоритетный select

func prioritySelect(priority chan int, normal chan int) {
select {
case val := <-priority:
fmt.Println("Priority:", val)
default:
// Если priority не готов, проверяем оба
select {
case val := <-priority:
fmt.Println("Priority:", val)
case val := <-normal:
fmt.Println("Normal:", val)
}
}
}

10. Распространённые ошибки

// ОШИБКА 1: Забыли остановить таймер
func forgotToStop() {
timer := time.NewTimer(1 * time.Second)
// defer timer.Stop() // Забыли!

<-timer.C
// Таймер продолжает существовать в памяти
}

// ОШИБКА 2: Чтение из сработавшего таймера без остановки
func readWithoutStop() {
timer := time.NewTimer(1 * time.Millisecond)
time.Sleep(10 * time.Millisecond)

// Таймер уже сработал, но мы не остановили его
// При повторном select может сработать немедленно
select {
case <-timer.C:
fmt.Println("Timer fired")
default:
// Может не выполниться, хотя таймер сработал
}

timer.Stop()
}

// ОШИБКА 3: Использование time.After в цикле
func afterInLoop() {
ch := make(chan int)

for {
select {
case val := <-ch:
return
case <-time.After(1 * time.Second):
// Каждую секунду создаётся новый таймер!
// При долгой работе — утечка памяти
}
}
}

// ПРАВИЛЬНО: используйте time.NewTimer
func timerInLoop() {
ch := make(chan int)
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()

for {
select {
case val := <-ch:
return
case <-timer.C:
// Перезапустить таймер для следующей итерации
timer.Reset(1 * time.Second)
}
}
}

11. Паттерн: graceful shutdown с select

func gracefulShutdown() {
done := make(chan struct{})
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// Рабочий процесс
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-done:
fmt.Println("Shutting down gracefully...")
// Очистка ресурсов
return
case <-ticker.C:
// Основная работа
}
}
}()

// Ожидание сигнала
<-sigChan
close(done)

// Даём время на завершение
time.Sleep(500 * time.Millisecond)
}

12. Сравнение подходов

ПодходПлюсыМинусы
time.AfterПростойУтечка таймеров в циклах
time.NewTimerКонтроль над таймеромНужно останавливать вручную
context.WithTimeoutРаспространяется по вызовамНакладные расходы
time.TickerДля периодических задачНужно останавливать

Итог: select с default обеспечивает неблокирующее поведение. time.After в циклах вызывает утечку таймеров — используйте time.NewTimer с Stop() или context.WithTimeout. При нескольких готовых каналах выбор недетерминирован. Для периодических задач используйте time.Ticker.

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

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

Ответ собеседника: Правильный. Буферизированный канал имеет буфер заданного размера — можно записать значения до заполнения буфера без блокировки. После заполнения запись блокируется до освобождения места. Для канала-семафора лучше использовать пустую структуру struct{}, так как она занимает 0 байт. Пустой интерфейс interface{} весит 16 байт и использует рефлексию, что менее эффективно.

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

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

1. Небуферизированный канал (синхронный)

func unbufferedChannel() {
ch := make(chan int) // Без буфера

// Запись блокируется до готовности получателя
go func() {
ch <- 42 // Блокируется, пока кто-то не прочитает
}()

val := <-ch // Получение разблокирует отправителя
fmt.Println(val)
}

Характеристики:

  • Гарантирует синхронизацию между отправителем и получателем
  • Отправитель блокируется до готовности получателя
  • Получатель блокируется до готовности отправителя
  • Используется для координации и синхронизации

2. Буферизированный канал (асинхронный)

func bufferedChannel() {
ch := make(chan int, 3) // Буфер на 3 элемента

// Запись не блокируется, пока буфер не полон
ch <- 1 // OK
ch <- 2 // OK
ch <- 3 // OK
// ch <- 4 // Блокируется — буфер полон

go func() {
for val := range ch {
fmt.Println(val)
}
}()
}

Характеристики:

  • Отправитель блокируется только когда буфер полон
  • Получатель блокируется только когда буфер пуст
  • Обеспечивает развязку между отправителем и получателем
  • Используется для сглаживания пиковых нагрузок

3. Сравнение поведения

func comparison() {
// Небуферизированный: строгая синхронизация
syncCh := make(chan int)

go func() {
for i := 0; i < 5; i++ {
syncCh <- i // Блокируется до получения
}
close(syncCh)
}()

for val := range syncCh {
fmt.Println("Sync:", val)
time.Sleep(100 * time.Millisecond) // Отправитель ждёт
}

// Буферизированный: развязка
asyncCh := make(chan int, 5)

go func() {
for i := 0; i < 5; i++ {
asyncCh <- i // Не блокируется, пока буфер не полон
}
close(asyncCh)
}()

time.Sleep(500 * time.Millisecond) // Отправитель уже завершился

for val := range asyncCh {
fmt.Println("Async:", val)
}
}

4. Канал-семафор с struct{}

Пустая структура struct{} — идеальный тип для сигналов без данных:

func semaphoreExample() {
// Семафор с ограничением на 3 одновременных выполнения
sem := make(chan struct{}, 3)

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

sem <- struct{}{} // Захват семафора
defer func() { <-sem }() // Освобождение семафора

fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait()
}

5. Почему struct{} лучше interface{}

func memoryComparison() {
// struct{} — 0 байт
var s struct{}
fmt.Println("struct{} size:", unsafe.Sizeof(s)) // 0

// interface{} — 16 байт (указатель на тип + указатель на данные)
var i interface{}
fmt.Println("interface{} size:", unsafe.Sizeof(i)) // 16

// bool — 1 байт
var b bool
fmt.Println("bool size:", unsafe.Sizeof(b)) // 1

// byte — 1 байт
var by byte
fmt.Println("byte size:", unsafe.Sizeof(by)) // 1
}

6. Практические примеры использования struct{}

// Сигнал завершения
func doneSignal() {
done := make(chan struct{})

go func() {
// Долгая работа
time.Sleep(2 * time.Second)
close(done) // Сигнал о завершении
}()

<-done // Ожидание завершения
fmt.Println("Done!")
}

// Множество сигналов завершения
func multipleDone() {
done1 := make(chan struct{})
done2 := make(chan struct{})

go func() {
time.Sleep(1 * time.Second)
close(done1)
}()

go func() {
time.Sleep(2 * time.Second)
close(done2)
}()

<-done1
fmt.Println("Task 1 done")
<-done2
fmt.Println("Task 2 done")
}

// Паттерn: одноразовый сигнал
type OnceSignal struct {
ch chan struct{}
}

func NewOnceSignal() *OnceSignal {
return &OnceSignal{ch: make(chan struct{})}
}

func (s *OnceSignal) Signal() {
close(s.ch)
}

func (s *OnceSignal) Wait() {
<-s.ch
}

func (s *OnceSignal) Done() bool {
select {
case <-s.ch:
return true
default:
return false
}
}

7. Когда использовать буферизированные каналы

// Паттерн: worker pool с ограничением очереди
func workerPoolWithBuffer() {
const maxWorkers = 5
const maxQueue = 20

jobs := make(chan Job, maxQueue)
results := make(chan Result, maxQueue)

// Запуск воркеров
for i := 0; i < maxWorkers; i++ {
go func() {
for job := range jobs {
results <- process(job)
}
}()
}

// Отправка задач с проверкой переполнения
go func() {
for _, job := range getAllJobs() {
select {
case jobs <- job:
// Задача принята
default:
// Очередь полна — обработка перегрузки
log.Println("Queue full, dropping job")
}
}
close(jobs)
}()
}

8. Когда использовать небуферизированные каналы

// Паттерн: запрос-ответ с гарантией доставки
func requestResponse() {
type Request struct {
Data string
Response chan Response
}

requests := make(chan Request) // Небуферизированный

go func() {
for req := range requests {
// Обработка запроса
result := processRequest(req.Data)
req.Response <- result // Гарантированная доставка
}
}()

// Отправка запроса
respCh := make(chan Response)
requests <- Request{Data: "query", Response: respCh}
result := <-respCh // Гарантированно получим ответ
}

9. Проблемы буферизированных каналов

// ПРОБЛЕМА: Буфер может скрыть проблемы с производительностью
func hiddenProblem() {
ch := make(chan int, 1000) // Большой буфер

go func() {
for i := 0; i < 1000000; i++ {
ch <- i // Не блокируется сразу
}
close(ch)
}()

// Обработка медленная, но буфер скрывает это
for val := range ch {
time.Sleep(time.Millisecond) // Медленная обработка
// Буфер заполняется, отправитель блокируется
// Но мы не видим проблему сразу
}
}

// ЛУЧШЕ: Маленький буфер или небуферизированный канал
func betterApproach() {
ch := make(chan int, 10) // Маленький буфер

go func() {
for i := 0; i < 1000000; i++ {
ch <- i // Быстро блокируется, если получатель медленный
}
close(ch)
}()

for val := range ch {
time.Sleep(time.Millisecond)
}
}

10. Таблица выбора типа канала

СценарийТип каналаТип данных
СинхронизацияНебуферизированныйstruct{}
СемафорБуферизированныйstruct{}
Передача данныхЗависит от задачиКонкретный тип
Сигнал завершенияНебуферизированныйstruct{}
Worker poolБуферизированныйКонкретный тип
Rate limiterБуферизированныйstruct{}

Итог: Небуферизированные каналы обеспечивают строгую синхронизацию, буферизированные — развязку и сглаживание нагрузки. Для каналов-сигналов и семафоров используйте struct{} — это 0 байт и семантически правильно. Избегайте больших буферов, которые скрывают проблемы с производительностью.

Вопрос 17. Что выведет следующий код с циклом for-range по слайсу указателей и почему?

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

Ответ собеседника: Правильный. Код выведет «40 40 40 40». Причина: в цикле for-range переменная v создаётся один раз и переиспользуется на каждой итерации. На каждой итерации в неё записывается указатель на текущее значение, но все элементы слайса ссылаются на одну и ту же переменную v. После цикла все указатели указывают на последнее значение — 40.

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

Это классическая ловушка в Go, связанная с тем, как работает for-range. Понимание этого механизма критически важно для написания корректного кода.

1. Проблемный код

func problematicCode() {
var ptrs []*int

for _, v := range []int{10, 20, 30, 40} {
ptrs = append(ptrs, &v)
}

// Выведет: 40 40 40 40
for _, p := range ptrs {
fmt.Print(*p, " ")
}
fmt.Println()
}

2. Почему так происходит

В Go for-range создаёт одну переменную и переиспользует её на каждой итерации:

// Как это работает внутри
func howItWorks() {
var ptrs []*int
slice := []int{10, 20, 30, 40}

// Range создаёт ОДНУ переменную v
var v int
for i := 0; i < len(slice); i++ {
v = slice[i] // Перезаписывает ту же переменную
ptrs = append(ptrs, &v) // Указатель на ту же переменную
}

// После цикла v == 40, все указатели ссылаются на v
}

3. Решение 1: Использовать индекс

func solutionWithIndex() {
var ptrs []*int
slice := []int{10, 20, 30, 40}

for i := range slice {
ptrs = append(ptrs, &slice[i])
}

// Выведет: 10 20 30 40
for _, p := range ptrs {
fmt.Print(*p, " ")
}
fmt.Println()
}

4. Решение 2: Создать локальную копию

func solutionWithCopy() {
var ptrs []*int

for _, v := range []int{10, 20, 30, 40} {
v := v // Создаём новую переменную в каждой итерации
ptrs = append(ptrs, &v)
}

// Выведет: 10 20 30 40
for _, p := range ptrs {
fmt.Print(*p, " ")
}
fmt.Println()
}

5. Решение 3: Использовать значение вместо указателя

func solutionWithValue() {
var values []int

for _, v := range []int{10, 20, 30, 40} {
values = append(values, v) // Копируем значение
}

// Выведет: 10 20 30 40
for _, v := range values {
fmt.Print(v, " ")
}
fmt.Println()
}

6. Аналогичная проблема с замыканиями

// ПРОБЛЕМА: Замыкания в цикле
func closureProblem() {
var funcs []func()

for _, v := range []int{10, 20, 30, 40} {
funcs = append(funcs, func() {
fmt.Print(v, " ")
})
}

// Выведет: 40 40 40 40
for _, f := range funcs {
f()
}
fmt.Println()
}

// РЕШЕНИЕ: Передать параметр
func closureSolution() {
var funcs []func()

for _, v := range []int{10, 20, 30, 40} {
v := v // Новая переменная в каждой итерации
funcs = append(funcs, func() {
fmt.Print(v, " ")
})
}

// Выведет: 10 20 30 40
for _, f := range funcs {
f()
}
fmt.Println()
}

7. Аналогичная проблема с горутинами

// ПРОБЛЕМА: Горутины в цикле
func goroutineProblem() {
var wg sync.WaitGroup

for _, v := range []int{10, 20, 30, 40} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Print(v, " ") // Захватывает переменную v
}()
}

wg.Wait()
// Выведет: 40 40 40 40 (или другой непредсказуемый результат)
fmt.Println()
}

// РЕШЕНИЕ: Передать параметр
func goroutineSolution() {
var wg sync.WaitGroup

for _, v := range []int{10, 20, 30, 40} {
wg.Add(1)
go func(val int) {
defer wg.Done()
fmt.Print(val, " ")
}(v) // Передаём значение как параметр
}

wg.Wait()
// Выведет: 10 20 30 40 (порядок может быть другим)
fmt.Println()
}

8. Когда это не проблема

// НЕ ПРОБЛЕМА: Не сохраняем указатель
func notAProblem() {
for _, v := range []int{10, 20, 30, 40} {
fmt.Print(v, " ") // Выведет: 10 20 30 40
}
fmt.Println()
}

// НЕ ПРОБЛЕМА: Используем значение сразу
func notAProblem2() {
var sum int
for _, v := range []int{10, 20, 30, 40} {
sum += v // Используем значение сразу
}
fmt.Println(sum) // 100
}

9. Go 1.22+ — исправление проблемы

Начиная с Go 1.22, переменные цикла создаются заново в каждой итерации:

// Go 1.22+ — это работает корректно
func go122Solution() {
var ptrs []*int

for _, v := range []int{10, 20, 30, 40} {
ptrs = append(ptrs, &v) // Каждый раз новая переменная
}

// Выведет: 10 20 30 40 (в Go 1.22+)
for _, p := range ptrs {
fmt.Print(*p, " ")
}
fmt.Println()
}

10. Рекомендации

  • Всегда используйте v := v при сохранении указателя на переменную цикла
  • Передавайте значения как параметры в замыкания и горутины
  • Используйте индексы вместо указателей на переменные цикла
  • Обновляйтесь до Go 1.22+, где эта проблема исправлена

Итог: for-range переиспользует переменную цикла, что приводит к неожиданному поведению при сохранении указателей или захвате переменной в замыканиях. Решения: использовать индексы, создавать локальные копии (v := v), или передавать значения как параметры. В Go 1.22+ эта проблема исправлена.

Вопрос 18. Что выведет код с запуском 5 горутин, которые пишут в буферизированный канал ёмкостью 3, и какие проблемы здесь есть?

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

Ответ собеседника: Правильный. Код выведет непредсказуемый результат — скорее всего пятёрки, но порядок и количество не гарантированы. Проблемы: 1) Замыкание захватывает переменную i по ссылке, поэтому к моменту выполнения горутины i уже равно 5. 2) Канал буферизирован на 3, но горутин 5 — часть заблокируется. 3) Нет синхронизации завершения горутин. Нужно передавать i как параметр функции и использовать WaitGroup.

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

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

1. Проблемный код

func problematicCode() {
ch := make(chan int, 3) // Буфер на 3

for i := 0; i < 5; i++ {
go func() {
ch <- i // Захватывает i по ссылке
}()
}

// Попытка прочитать 5 значений
for i := 0; i < 5; i++ {
fmt.Print(<-ch, " ")
}
fmt.Println()
}

2. Проблемы в коде

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

// Как это работает внутри
func closureCapture() {
ch := make(chan int, 3)

// Переменная i одна для всех итераций
var i int
for i = 0; i < 5; i++ {
// Горутина захватывает указатель на i
go func() {
ch <- i // Читает текущее значение i
}()
}

// К моменту выполнения горутин, i уже равно 5
// Все горутины могут увидеть i == 5
}

Проблема 2: Буфер канала меньше количества горутин

func bufferIssue() {
ch := make(chan int, 3) // Буфер на 3

for i := 0; i < 5; i++ {
go func(id int) {
ch <- id // Первые 3 запишутся в буфер
// Остальные 2 заблокируются до чтения
}(i)
}

// Если не прочитать все 5 значений — утечка горутин
}

Проблема 3: Нет синхронизации завершения

func noSynchronization() {
ch := make(chan int, 3)

for i := 0; i < 5; i++ {
go func(id int) {
ch <- id
// Горутина может не завершиться до выхода из main
}(i)
}

// Читаем только 3 значения из буфера
for i := 0; i < 3; i++ {
fmt.Print(<-ch, " ")
}
// Выход — остальные горутины зависают навсегда
}

3. Исправленный код

func correctCode() {
ch := make(chan int, 3)
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) { // Передаём параметр
defer wg.Done()
ch <- id
}(i) // Передаём значение
}

// Закрываем канал после завершения всех горутин
go func() {
wg.Wait()
close(ch)
}()

// Читаем все значения
for val := range ch {
fmt.Print(val, " ")
}
fmt.Println()
}

4. Что может вывести проблемный код

// Возможные варианты вывода:
// 5 5 5 5 5 — все горутины увидели i == 5
// 0 1 2 3 4 — если горутины выполнились мгновенно (редко)
// 3 4 4 4 5 — непредсказуемый результат
// Или вообще deadlock, если не прочитать все значения

5. Демонстрация проблемы с замыканием

func demonstrateClosureIssue() {
// ПРОБЛЕМА
var funcs []func()
for i := 0; i < 5; i++ {
funcs = append(funcs, func() {
fmt.Print(i, " ") // Выведет: 5 5 5 5 5
})
}
for _, f := range funcs {
f()
}
fmt.Println()

// РЕШЕНИЕ 1: Локальная переменная
var funcs2 []func()
for i := 0; i < 5; i++ {
i := i // Новая переменная в каждой итерации
funcs2 = append(funcs2, func() {
fmt.Print(i, " ") // Выведет: 0 1 2 3 4
})
}
for _, f := range funcs2 {
f()
}
fmt.Println()

// РЕШЕНИЕ 2: Параметр функции
var funcs3 []func()
for i := 0; i < 5; i++ {
funcs3 = append(funcs3, func(val int) func() {
return func() {
fmt.Print(val, " ") // Выведет: 0 1 2 3 4
}
}(i))
}
for _, f := range funcs3 {
f()
}
fmt.Println()
}

6. Демонстрация проблемы с буфером

func demonstrateBufferIssue() {
ch := make(chan int, 3)

// Отправляем 5 значений в буфер на 3
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Printf("Goroutine %d: sending\n", id)
ch <- id
fmt.Printf("Goroutine %d: sent\n", id)
}(i)
}

// Даём время на отправку
time.Sleep(100 * time.Millisecond)

// Читаем только 3 значения
for i := 0; i < 3; i++ {
fmt.Println("Received:", <-ch)
}

// Остальные 2 горутины заблокированы!
// Они будут ждать вечно, если мы не прочитаем
fmt.Println("Done reading")
}

7. Паттерн: правильная работа с горутинами и каналами

func properPattern() {
const numWorkers = 5

jobs := make(chan int, numWorkers)
results := make(chan int, numWorkers)

// Запуск воркеров
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
result := job * 2
results <- result
}
}(i)
}

// Отправка задач
go func() {
for i := 0; i < numWorkers; i++ {
jobs <- i
}
close(jobs)
}()

// Закрытие results после завершения воркеров
go func() {
wg.Wait()
close(results)
}()

// Сбор результатов
for result := range results {
fmt.Print(result, " ")
}
fmt.Println()
}

8. Проверка с помощью -race detector

# Запуск с детектором гонок
go run -race main.go

Проблемный код будет обнаружен как data race:

WARNING: DATA RACE
Write at 0x00c0000b4010 by main goroutine:
main.problematicCode()
Previous read at 0x00c0000b4010 by goroutine 7:
main.problematicCode.func1()

9. Рекомендации

  • Всегда передавайте параметры в горутины явно
  • Используйте sync.WaitGroup для синхронизации завершения
  • Закрывайте каналы после завершения всех отправителей
  • Используйте -race для обнаружения гонок данных
  • Убедитесь, что буфер канала достаточен или есть читатели

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

Вопрос 19. Как использовать WaitGroup для синхронизации горутин и в чём была ошибка в примере с каналом?

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

Ответ собеседника: Правильный. WaitGroup используется для ожидания завершения группы горутин: Add() добавляет счётчик, Done() уменьшает, Wait() блокирует до завершения всех. В примере ошибка: горутины записывали в канал-семафор до начала полезной работы, а не после. Также переменная i захватывалась по ссылке в замыкании, что приводило к непредсказуемым значениям. Нужно передавать i как параметр и вызывать Done() после выполнения работы.

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

sync.WaitGroup — это основной инструмент для ожидания завершения группы горутин. Понимание его работы и типичных ошибок критически важно для написания корректного конкурентного кода.

1. Базовое использование WaitGroup

func basicWaitGroup() {
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1) // Увеличиваем счётчик
go func(id int) {
defer wg.Done() // Уменьшаем счётчик при завершении
fmt.Printf("Worker %d done\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
}(i)
}

wg.Wait() // Блокируемся, пока счётчик не станет 0
fmt.Println("All workers done")
}

2. Правильный порядок вызовов

func correctOrder() {
var wg sync.WaitGroup

// Add() вызываем ДО запуска горутины
wg.Add(1)
go func() {
// Done() вызываем ПОСЛЕ завершения работы
defer wg.Done()
// Работа
}()

wg.Wait()
}

3. Типичные ошибки

Ошибка 1: Add() внутри горутины

func wrongAddInside() {
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // ОШИБКА: Add внутри горутины!
defer wg.Done()
// Работа
}(i)
}

wg.Wait() // Может завершиться раньше, чем начнутся горутины
}

Проблема: Wait() может завершиться до того, как горутины вызовут Add().

Ошибка 2: Забыли Done()

func forgotDone() {
var wg sync.WaitGroup

wg.Add(1)
go func() {
// defer wg.Done() // Забыли!
// Работа
}()

wg.Wait() // Deadlock! Счётчик никогда не станет 0
}

Ошибка 3: Done() вызывается до завершения работы

func earlyDone() {
var wg sync.WaitGroup

wg.Add(1)
go func() {
wg.Done() // ОШИБКА: Done до завершения работы!
// Долгая работа
time.Sleep(5 * time.Second)
}()

wg.Wait() // Завершится сразу, но горутина ещё работает
}

Ошибка 4: Копирование WaitGroup

func copyWaitGroup(wg sync.WaitGroup) { // ОШИБКА: Передача по 값ению!
wg.Add(1)
go func() {
defer wg.Done()
// Работа
}()
}

func main() {
var wg sync.WaitGroup
copyWaitGroup(wg) // Копия — Done() не влияет на оригинал
wg.Wait() // Deadlock!
}

4. Правильный паттерн с каналом-семафором

func semaphoreWithWaitGroup() {
const maxConcurrent = 3
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

sem <- struct{}{} // Захват семафора
defer func() { <-sem }() // Освобождение

fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished\n", id)
}(i)
}

wg.Wait()
fmt.Println("All done")
}

5. Паттерн: ограничение конкурентности с результатами

func concurrentWithResults() {
const maxConcurrent = 3
sem := make(chan struct{}, maxConcurrent)
results := make(chan int, 10)
var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

sem <- struct{}{}
defer func() { <-sem }()

// Работа
result := id * 2
results <- result
}(i)
}

// Закрываем results после завершения всех горутин
go func() {
wg.Wait()
close(results)
}()

// Сбор результатов
for result := range results {
fmt.Println("Result:", result)
}
}

6. Паттерн: обработка ошибок с WaitGroup

func withErrorHandling() {
var wg sync.WaitGroup
errChan := make(chan error, 10)

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

if err := doWork(id); err != nil {
errChan <- fmt.Errorf("worker %d: %w", id, err)
}
}(i)
}

// Закрываем errChan после завершения
go func() {
wg.Wait()
close(errChan)
}()
}

7. Паттерн: контекст с WaitGroup

func withContext() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()

select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled\n", id)
return
case <-time.After(time.Duration(id) * time.Second):
fmt.Printf("Worker %d done\n", id)
}
}(i)
}

wg.Wait()
}

8. Сравнение подходов синхронизации

ПодходКогда использовать
WaitGroupОжидание завершения группы горутин
КаналПередача данных + синхронизация
ContextОтмена и таймауты
MutexЗащита общего состояния

9. Рекомендации

  • Вызывайте Add() до запуска горутины
  • Используйте defer wg.Done() в начале горутины
  • Передавайте WaitGroup по указателю
  • Не копируйте WaitGroup
  • Используйте context для отмены длительных операций

Итог: WaitGroup — простой счётчик для ожидания завершения горутин. Add() до запуска, Done() при завершении, Wait() для блокировки. Типичные ошибки: Add() внутри горутины, забытый Done(), копирование WaitGroup. Для ограничения конкурентности комбинируйте с каналом-семафором.

Вопрос 20. Как работает defer и в каком порядке выполняются несколько defer?

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

Ответ собеседника: Правильный. Defer откладывает вызов функции до выхода из текущей функции. Несколько defer выполняются в порядке LIFO (последний добавленный — первый выполненный). Defer полезен для освобождения ресурсов (закрытие файлов, соединений) сразу после их инициализации, чтобы не забыть освободить при множественных точках выхода. Также defer с recover используется для перехвата паник.

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

defer — это мощный механизм для отложенного выполнения кода. Он гарантирует выполнение функции при выходе из текущей функции, независимо от способа выхода (return, panic).

1. Базовое поведение

func basicDefer() {
defer fmt.Println("Third")
defer fmt.Println("Second")
defer fmt.Println("First")

fmt.Println("Function body")
}

// Вывод:
// Function body
// First
// Second
// Third

2. Порядок выполнения — LIFO

func lifoOrder() {
for i := 0; i < 5; i++ {
defer fmt.Print(i, " ")
}
fmt.Println("done")
}

// Вывод: done 4 3 2 1 0

3. Вычисление аргументов defer

Аргументы вычисляются сразу, но функция вызывается позже:

func argumentEvaluation() {
i := 0
defer fmt.Println("deferred:", i) // i вычислен сейчас: 0

i = 42
fmt.Println("current:", i) // 42
}

// Вывод:
// current: 42
// deferred: 0

4. Defer с замыканиями

func closureDefer() {
i := 0
defer func() {
fmt.Println("deferred:", i) // Замыкание читает актуальное значение
}()

i = 42
fmt.Println("current:", i)
}

// Вывод:
// current: 42
// deferred: 42

5. Изменение именованных возвращаемых значений

func namedReturn() (result int) {
defer func() {
result++ // Может изменить возвращаемое значение
}()

return 41 // result = 41, затем defer увеличивает до 42
}

func main() {
fmt.Println(namedReturn()) // 42
}

6. Практическое использование: освобождение ресурсов

func resourceManagement() {
// Файл
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close() // Гарантированное закрытие

// Мьютекс
var mu sync.Mutex
mu.Lock()
defer mu.Unlock() // Гарантированная разблокировка

// Соединение с БД
db, err := sql.Open("postgres", "...")
if err != nil {
log.Fatal(err)
}
defer db.Close()

// Работа с ресурсами...
}

7. Defer для логирования входа/выхода

func logEntryExit() {
defer func() {
fmt.Println("Exiting function")
}()
fmt.Println("Entering function")
// Работа...
}

// Вывод:
// Entering function
// Exiting function

8. Defer с recover для перехвата паник

func panicRecovery() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()

fmt.Println("Before panic")
panic("something went wrong")
fmt.Println("After panic") // Не выполнится
}

// Вывод:
// Before panic
// Recovered from panic: something wrong

9. Паттерn: транзакции с defer

func transactionPattern(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}

// Откат при панике или ошибке
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // Пробрасываем панику дальше
}
}()

// Операции с транзакцией
if _, err := tx.Exec("INSERT ..."); err != nil {
tx.Rollback()
return err
}

if _, err := tx.Exec("UPDATE ..."); err != nil {
tx.Rollback()
return err
}

return tx.Commit()
}

10. Паттерn: замер времени выполнения

func measureTime(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}

func expensiveOperation() {
defer measureTime("expensiveOperation")()
// Долгая работа
time.Sleep(time.Second)
}

// Вывод: expensiveOperation took 1.001s

11. Распространённые ошибки

// ОШИБКА 1: Defer в цикле
func deferInLoop() {
for i := 0; i < 100; i++ {
file, _ := os.Open(fmt.Sprintf("file_%d.txt", i))
// defer file.Close() // ОШИБКА: Все файлы закроются только в конце функции!
}
}

// ПРАВИЛЬНО: Оборачивайте в функцию
func noDeferInLoop() {
for i := 0; i < 100; i++ {
func() {
file, _ := os.Open(fmt.Sprintf("file_%d.txt", i))
defer file.Close() // Закрывается в каждой итерации
// Работа с файлом
}()
}
}

// ОШИБКА 2: Defer после проверки ошибки
func deferAfterError() {
file, err := os.Open("file.txt")
// defer file.Close() // ОШИБКА: file может быть nil!
if err != nil {
return
}
defer file.Close() // ПРАВИЛЬНО: после проверки
}

// ОШИБКА 3: Забыли проверить ошибку в defer
func deferWithoutErrorCheck() {
file, err := os.Open("file.txt")
if err != nil {
return
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("Error closing file: %v", err)
}
}()
}

12. Производительность defer

До Go 1.13 defer имел значительные накладные расходы (~100ns). С Go 1.13+ оптимизация сделала defer практически бесплатным (~30ns) в большинстве случаев.

func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
func() {
defer func() {}()
}()
}
}

func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
func() {}()
}
}

13. Таблица поведения defer

СценарийПоведение
returndefer выполняется до возврата
panicdefer выполняется до распространения паники
os.Exitdefer НЕ выполняется
Несколько deferLIFO порядок
АргументыВычисляются сразу
ЗамыканияЧитают актуальные значения

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

Вопрос 21. Что произойдёт при чтении из канала, в который никто не пишет?

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

Ответ собеседника: Правильный. Горутина заблокируется навсегда (deadlock), ожидая данные из канала. Если все горутины заблокируются, Go runtime обнаружит deadlock и завершит программу с паникой. Для избежания этого нужно использовать select с таймаутом или контекст с дедлайном.

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

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

1. Базовое поведение блокировки

func basicBlocking() {
ch := make(chan int)

// Это заблокирует горутина навсегда
val := <-ch
fmt.Println(val) // Никогда не выполнится
}

2. Deadlock при блокировке всех горутин

func deadlock() {
ch := make(chan int)

// Главная горутина блокируется
<-ch

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

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

3. Частичный deadlock

func partialDeadlock() {
ch := make(chan int)

// Горутина блокируется на канале
go func() {
<-ch // Заблокируется навсегда
}()

// Главная горутина продолжает работать
fmt.Println("Main continues")
time.Sleep(time.Second)
// Горутина всё ещё заблокирована — утечка горутины!
}

4. Решение 1: select с таймаутом

func withTimeout() {
ch := make(chan int)

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-time.After(5 * time.Second):
fmt.Println("Timeout - no data received")
}
}

5. Решение 2: context с дедлайном

func withContext() {
ch := make(chan int)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-ctx.Done():
fmt.Println("Context cancelled:", ctx.Err())
}
}

6. Решение 3: неблокирующее чтение

func nonBlockingRead() {
ch := make(chan int, 1)

select {
case val := <-ch:
fmt.Println("Received:", val)
default:
fmt.Println("No data available")
}
}

7. Решение 4: закрытие канала как сигнал

func closeAsSignal() {
done := make(chan struct{})

// Горутина ждёт сигнала завершения
go func() {
<-done
fmt.Println("Received close signal")
}()

// Сигнал к завершению
close(done)
time.Sleep(100 * time.Millisecond)
}

8. Паттерn: worker с graceful shutdown

func workerWithShutdown() {
jobs := make(chan int, 10)
done := make(chan struct{})

// Worker
go func() {
for {
select {
case job, ok := <-jobs:
if !ok {
fmt.Println("Jobs channel closed, exiting")
return
}
fmt.Println("Processing job:", job)
case <-done:
fmt.Println("Shutdown signal received, exiting")
return
}
}
}()

// Отправка задач
for i := 0; i < 5; i++ {
jobs <- i
}

// Graceful shutdown
close(done)
time.Sleep(100 * time.Millisecond)
}

9. Паттерn: pipeline с отменой

func pipelineWithCancel() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Генератор
generate := func() <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case <-ctx.Done():
return
}
}
}()
return ch
}

// Обработчик
process := func(in <-chan int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for val := range in {
select {
case ch <- val * 2:
case <-ctx.Done():
return
}
}
}()
return ch
}

// Pipeline
for result := range process(generate()) {
fmt.Println(result)
if result >= 10 {
cancel() // Отмена
break
}
}
}

10. Обнаружение утечек горутин

func detectGoroutineLeak() {
ch := make(chan int)

// Запускаем горутину, которая заблокируется
go func() {
<-ch // Заблокируется навсегда
}()

// Проверяем количество горутин
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutines:", runtime.NumGoroutine())

// Горутина утечёт, если мы не закроем канал
// close(ch) // Раскомментируйте, чтобы исправить
}

11. Использование runtime для отладки

func debugGoroutines() {
ch := make(chan int)

go func() {
<-ch
}()

// Получить стек всех горутин
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true)
fmt.Printf("Goroutine dump:\n%s\n", buf[:n])
}

12. Таблица поведения каналов

Состояние каналаЧтениеЗапись
Открытый, пустойБлокируетЗаписывает
Открытый, полныйЧитаетБлокирует
Открытый, частично заполненЧитаетЗаписывает
Закрытый, пустойZero value, falsePanic
Закрытый, не пустойЧитает оставшиесяPanic
nilБлокирует навсегдаБлокирует навсегда

13. Рекомендации

  • Всегда предусматривайте таймауты при чтении из каналов
  • Используйте context для отмены длительных операций
  • Закрывайте каналы для сигнализации о завершении
  • Используйте select с default для неблокирующего чтения
  • Мониторьте количество горутин в production

Итог: Чтение из канала, в который никто не пишет, блокирует горутина навсегда. Если все горутины заблокированы — deadlock. Для предотвращения используйте таймауты, контексты, неблокирующее чтение и закрытие каналов для сигнализации.

Вопрос 22. Что не так с кодом, который запускает 100 горутин с таймаутом в наносекунду и использует runtime.Gosched()?

Таймкод: 01:02:19

Ответ собеседника: Правильный. Проблема в утечке горутин: при маленьком таймауте select всегда попадает в ветку тайм-аута, но горутина с «тяжёлым запросом» продолжает работать в фоне. После завершения main эти горутины не завершаются, а продолжают существовать, потребляя ресурсы. runtime.Gosched() не решает проблему. Нужно использовать контекст с отменой для корректного завершения горутин.

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

Эта задача демонстрирует классическую проблему утечки горутин и непонимания работы планировщика Go. Давайте разберём все аспекты.

1. Проблемный код

func problematicCode() {
for i := 0; i < 100; i++ {
go func(id int) {
ch := make(chan int)

// Тяжёлая работа в отдельной горутине
go func() {
time.Sleep(time.Second) // Имитация долгой работы
ch <- id
}()

// Таймаут в наносекунду — всегда сработает первым
select {
case val := <-ch:
fmt.Println("Received:", val)
case <-time.After(time.Nanosecond):
fmt.Println("Timeout for", id)
}

runtime.Gosched() // Не помогает!
}(i)
}

time.Sleep(2 * time.Second)
}

2. Проблемы в коде

Проблема 1: Утечка горутин

func goroutineLeak() {
ch := make(chan int)

// Горутина запускается
go func() {
time.Sleep(time.Second)
ch <- 42 // Заблокируется навсегда после таймаута
}()

select {
case val := <-ch:
fmt.Println(val)
case <-time.After(time.Nanosecond):
fmt.Println("Timeout")
// Горутина всё ещё ждёт возможности отправить в ch!
// Она никогда не завершится — утечка
}
}

Проблема 2: runtime.Gosched() не помогает

func goschedDoesNotHelp() {
// runtime.Gosched() просто уступает процессорное время
// другим горутинам, но НЕ завершает текущую горутину!

go func() {
for {
fmt.Println("Running")
runtime.Gosched() // Уступает CPU, но не завершает
}
}()
}

Проблема 3: Наносекундный таймаут

func nanosecondTimeout() {
// time.After(time.Nanosecond) создаёт таймер на 1 наносекунду
// Это настолько мало, что таймер сработает практически мгновенно
// Горутина с тяжёлой работой не успеет завершиться

ch := make(chan int)
go func() {
time.Sleep(time.Second)
ch <- 42
}()

select {
case <-ch:
// Никогда не выполнится
case <-time.After(time.Nanosecond):
// Всегда выполняется
}
}

3. Исправленный код с контекстом

func correctWithContext() {
for i := 0; i < 100; i++ {
go func(id int) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

ch := make(chan int, 1) // Буферизованный!

// Горутина с поддержкой отмены
go func() {
select {
case <-ctx.Done():
return // Отмена — выходим
case <-time.After(500 * time.Millisecond):
// Работа завершилась
}

select {
case ch <- id:
case <-ctx.Done():
return
}
}()

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-ctx.Done():
fmt.Println("Timeout or cancelled for", id)
}
}(i)
}

time.Sleep(2 * time.Second)
}

4. Исправленный код с буферизованным каналом

func correctWithBufferedChannel() {
for i := 0; i < 100; i++ {
go func(id int) {
ch := make(chan int, 1) // Буфер на 1 — запись не блокируется

go func() {
time.Sleep(500 * time.Millisecond)
ch <- id // Запишется в буфер без блокировки
}()

select {
case val := <-ch:
fmt.Println("Received:", val)
case <-time.After(time.Second):
fmt.Println("Timeout for", id)
}

// Горутина с тяжёлой работой завершится и запишет в буфер
// Канал будет собран GC
}(i)
}

time.Sleep(2 * time.Second)
}

5. Паттерn: правильная работа с таймаутами

func properTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := doWorkWithContext(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Work timed out")
}
return
}
fmt.Println("Result:", result)
}

func doWorkWithContext(ctx context.Context) (int, error) {
ch := make(chan int, 1)

go func() {
// Долгая работа
result := heavyComputation()
select {
case ch <- result:
case <-ctx.Done():
return
}
}()

select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}

6. Паттерn: worker pool с отменой

func workerPoolWithCancellation() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

const numWorkers = 10
jobs := make(chan int, 100)
results := make(chan int, 100)

// Запуск воркеров
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
result := process(job)
select {
case results <- result:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
}

// Отправка задач
go func() {
for i := 0; i < 100; i++ {
select {
case jobs <- i:
case <-ctx.Done():
break
}
}
close(jobs)
}()

// Сбор результатов
go func() {
wg.Wait()
close(results)
}()

// Обработка результатов
for result := range results {
fmt.Println(result)
}
}

7. Мониторинг утечек горутин

func monitorGoroutines() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()

go func() {
for range ticker.C {
n := runtime.NumGoroutine()
fmt.Printf("Goroutines: %d\n", n)
if n > 1000 {
log.Println("WARNING: Too many goroutines!")
}
}
}()
}

8. Использование pprof для отладки

import _ "net/http/pprof"

func enablePprof() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}

// Затем:
// go tool pprof http://localhost:6060/debug/pprof/goroutine

9. Таблица решений

ПроблемаРешение
Утечка горутинИспользовать context с отменой
Блокировка на каналеБуферизованные каналы или select
runtime.Gosched()Не решает проблему утечек
Маленький таймаутИспользовать адекватные значения

10. Рекомендации

  • Всегда используйте context для отмены горутин
  • Используйте буферизованные каналы, если отправитель может завершиться раньше получателя
  • Не полагайтесь на runtime.Gosched() для управления горутинами
  • Мониторьте количество горутин в production
  • Используйте pprof для отладки утечек

Итог: Проблемный код создаёт утечку горутин, потому что горутины с тяжёлой работой продолжают существовать после таймаута. runtime.Gosched() не помогает — он только уступает CPU. Решение: использовать context с отменой или буферизованные каналы.

Вопрос 23. Что такое горутина и почему она легковеснее потоков ОС?

Таймкод: 01:05:43

Ответ собеседника: Правильный. Горутина — это легковесный поток, управляемый рантаймом Go, а не операционной системой. Горутины выполняются на уровне языка с собственным планировщиком, который распределяет их по потокам ОС (M машин). Переключение контекста между горутинами дешевле, чем между потоками ОС, так как не требует подмены регистров процессора и работы с ядром. Стек горутины начинается с ~4 КБ и может расти.

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

Горутины — это фундаментальная абстракция Go для конкурентного программирования. Они реализуют модель CSP (Communicating Sequential Processes) и значительно отличаются от потоков ОС.

1. Что такое горутина

Горутина — это функция, которая выполняется конкурентно с другими горутинами в том же адресном пространстве. Она управляется рантаймом Go, а не операционной системой.

func main() {
// Запуск горутины
go func() {
fmt.Println("Hello from goroutine")
}()

// Главная горутина
fmt.Println("Hello from main")

time.Sleep(time.Millisecond) // Даём время для выполнения
}

2. Сравнение горутин и потоков ОС

ХарактеристикаГорутинаПоток ОС
Размер стека~2-8 КБ (начальный)~1-8 МБ (фиксированный)
Создание~200 нс~10-100 мкс
Переключение контекста~200 нс~1-10 мкс
Максимальное количествоСотни тысячТысячи
ПланировщикGo runtime (user-space)Ядро ОС (kernel-space)

3. Модель GMP

Go использует модель GMP для управления горутинами:

// G (goroutine) — горутина
// M (machine) — поток ОС
// P (processor) — процессор (контекст выполнения)

// Упрощённая схема:
// G1, G2, G3 -> P1 -> M1 (поток ОС)
// G4, G5, G6 -> P2 -> M2 (поток ОС)
// G7, G8, G9 -> P3 -> M3 (поток ОС)
func demonstrateGMP() {
// Количество P по умолчанию = количество CPU
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

// Установка количества P
runtime.GOMAXPROCS(4)
}

4. Стек горутины

В отличие от потоков ОС с фиксированным стеком, горутины имеют динамический стек:

func demonstrateStack() {
var wg sync.WaitGroup

// Можно запустить сотни тысяч горутин
for i := 0; i < 100000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Каждая горутина начинается с маленького стека
// Стек растёт по мере необходимости
time.Sleep(time.Millisecond)
}(i)
}

wg.Wait()
fmt.Println("All goroutines completed")
}

5. Переключение контекста

Переключение между горутинами происходит в user-space, без обращения к ядру:

func contextSwitch() {
ch := make(chan int)

// Горутина 1
go func() {
for i := 0; i < 1000; i++ {
ch <- i
}
}()

// Горутина 2
go func() {
for i := 0; i < 1000; i++ {
<-ch
}
}()

// Переключение происходит:
// 1. Когда горутина блокируется на канале
// 2. При вызове runtime.Gosched()
// 3. При системных вызовах
// 4. По таймеру (каждые ~10 мс)
}

6. Точки прерывания (preemption points)

Горутина может быть прервана в определённых точках:

func preemptionPoints() {
// Горутина будет прервана при:
// 1. Операциях с каналами (send/receive)
// 2. Системных вызовах
// 3. Вызове runtime.Gosched()
// 4. Вызове time.Sleep()
// 5. По таймеру (начиная с Go 1.14)

go func() {
for {
// Долгий цикл без точек прерывания
// До Go 1.14 это могло заблокировать другие горутины
// С Go 1.14+ горутина прерывается по таймеру
}
}()
}

7. Планировщик Go

func schedulerDetails() {
// Планировщик Go использует work-stealing алгоритм
// Каждый P имеет локальную очередь горутин
// Если очередь пуста, P ворует горутины у других P

runtime.GOMAXPROCS(2) // 2 процессора

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d running\n", id)
time.Sleep(100 * time.Millisecond)
}(i)
}
wg.Wait()
}

8. Системные вызовы

При системных вызовах горутина блокируется, но поток ОС может быть освобождён:

func syscallHandling() {
// Когда горутина делает системный вызов:
// 1. M (поток ОС) блокируется на системном вызове
// 2. P (процессор) отсоединяется от M
// 3. P присоединяется к другому M или создаёт новый M
// 4. Другие горутины продолжают выполняться

go func() {
// Системный вызов (чтение файла)
data, _ := os.ReadFile("large_file.txt")
fmt.Println("Read", len(data), "bytes")
}()

// Другие горутины продолжают работать
go func() {
fmt.Println("Other goroutine running")
}()
}

9. Преимущества горутин

func goroutineAdvantages() {
// 1. Малое потребление памяти
// 100 000 горутин * 4 КБ = ~400 МБ
// vs
// 100 000 потоков * 1 МБ = ~100 ГБ (невозможно!)

// 2. Быстрое создание и уничтожение
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
}()
}
wg.Wait()
fmt.Printf("Created 100k goroutines in %v\n", time.Since(start))

// 3. Простое управление
// Не нужно думать о пулах потоков, очередях задач и т.д.
}

10. Ограничения горутин

func goroutineLimitations() {
// 1. Горутина не имеет идентификатора
// Нельзя убить конкретную горутину извне

// 2. Нет гарантии порядка выполнения
go fmt.Print("a")
go fmt.Print("b")
go fmt.Print("c")
// Порядок вывода непредсказуем

// 3. Горутина может утечь
ch := make(chan int)
go func() {
val := <-ch // Заблокируется навсегда
fmt.Println(val)
}()
// Если никто не запишет в ch — утечка горутины
}

11. Рекомендации

  • Горутины дешёвые, но не бесплатные — не создавайте миллионы без необходимости
  • Используйте context для отмены горутин
  • Используйте sync.WaitGroup для ожидания завершения
  • Мониторьте количество горутин в production
  • Избегайте утечек горутин с помощью таймаутов и отмены

Итог: Горутина — легковесный поток, управляемый рантаймом Go. Она имеет маленький стек (~4 КБ), быстро создаётся (~200 нс) и переключается (~200 нс). Модель GMP позволяет эффективно распределять горутины по потокам ОС. Можно запускать сотни тысяч горутин в одной программе.

Вопрос 24. Как работает планировщик Go и что такое work stealing?

Таймкод: 01:10:50

Ответ собеседника: Правильный. Планировщик Go создаёт M машин (потоков ОС), обычно равное количеству ядер процессора. Каждая машина имеет локальную очередь задач и есть глобальная очередь. Планировщик умный — периодически переключает задачи (вытеснение). Если машина скучает и её локальная очередь пуста, она может забрать задачи из глобальной очереди или из очереди соседней машины — это называется work stealing. Приоритет у локальных задач, затем глобальные, и в последнюю очередь — stealing.

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

Планировщик Go — это один из ключевых компонентов runtime, который обеспечивает эффективное выполнение горутин. Он использует модель GMP и алгоритм work stealing для балансировки нагрузки.

1. Модель GMP

// G (Goroutine) — горутина
// M (Machine) — поток ОС
// P (Processor) — логический процессор

type g struct {
stack stack // стек горутины
stackguard0 uintptr // граница стека
m *m // M, на котором выполняется
sched gobuf // контекст планировщика
// ...
}

type p struct {
id int32
status uint32
m *m // M, к которому привязан
runqhead uint32 // голова локальной очереди
runqtail uint32 // хвост локальной очереди
runq [256]guintptr // локальная очередь (256 элементов)
runnext guintptr // следующая горутина для выполнения
// ...
}

type m struct {
g0 *g // горутина планировщика
curg *g // текущая выполняемая горутина
p puintptr // привязанный P
nextp puintptr // следующий P
// ...
}

2. Структура планировщика

func demonstrateScheduler() {
// Количество P = GOMAXPROCS
numP := runtime.GOMAXPROCS(0)
fmt.Printf("Number of P: %d\n", numP)

// Количество M может быть больше P
// M создаются при необходимости (системные вызовы, блокировки)

// Каждый P имеет локальную очередь на 256 горутин
// Если очередь полна, горутины попадают в глобальную очередь
}

3. Локальные и глобальные очереди

func queueBehavior() {
// Когда горутина создаётся или становится готовой:
// 1. Помещается в локальную очередь текущего P
// 2. Если локальная очередь полна — в глобальную очередь
// 3. Приоритет: runnext > локальная очередь > глобальная очередь

runtime.GOMAXPROCS(2) // 2 P

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Millisecond)
}(i)
}
wg.Wait()
}

4. Work Stealing — алгоритм воровства работы

func workStealingExplanation() {
// Когда P заканчивает горутины в локальной очереди:
// 1. Проверяет глобальную очередь
// 2. Если пусто — ворует у других P

// Алгоритм воровства:
// - Выбирается случайный P
// - Берётся половина горутин из его очереди
// - Это снижает конкуренцию между P

runtime.GOMAXPROCS(4) // 4 P

var wg sync.WaitGroup

// Создаём много горутин
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Имитация работы
sum := 0
for j := 0; j < 1000; j++ {
sum += j
}
}(i)
}

wg.Wait()
}

5. Приоритеты планирования

func schedulingPriorities() {
// Приоритет выбора горутины для выполнения:
// 1. runnext — следующая горутина (если есть)
// 2. Локальная очередь P
// 3. Глобальная очередь (каждые 61 итерация)
// 4. Work stealing у других P

// Проверка глобальной очереди каждые 61 итерация
// предотвращает голодание горутин в глобальной очереди
}

6. Вытеснение (preemption)

func preemptionDetails() {
// До Go 1.14: кооперативное вытеснение
// Горутина прерывалась только в точках вытеснения:
// - Вызовы функций
// - Операции с каналами
// - Системные вызовы

// С Go 1.14+: вытеснение по сигналу (SIGURG)
// Горутина прерывается через ~10 мс

go func() {
// Этот цикл будет прерван через ~10 мс
for {
// Тяжёлые вычисления без вызовов функций
}
}()
}

7. Системные вызовы и планировщик

func syscallHandling() {
// Когда горутина делает блокирующий системный вызов:
// 1. M блокируется на системном вызове
// 2. P отсоединяется от M
// 3. P ищет свободный M или создаёт новый
// 4. Другие горутины продолжают выполняться

go func() {
// Блокирующий системный вызов
data, _ := os.ReadFile("large_file.txt")
fmt.Println("Read", len(data), "bytes")
}()

// Другие горутины продолжают работать
go func() {
fmt.Println("Other goroutine running")
}()
}

8. Network poller

func networkPoller() {
// Go использует network poller (epoll/kqueue/IOCP)
// для неблокирующих сетевых операций

// Когда горутина ждёт сетевых данных:
// 1. Горутина приостанавливается
// 2. M не блокируется — может выполнять другие горутины
// 3. Когда данные готовы, горутина возобновляется

go func() {
resp, _ := http.Get("https://example.com")
defer resp.Body.Close()
// Горутина приостанавливалась во время ожидания ответа
// M выполнял другие горутины
}()
}

9. Мониторинг планировщика

func monitorScheduler() {
// Количество горутин
fmt.Println("Goroutines:", runtime.NumGoroutine())

// Количество потоков ОС
fmt.Println("OS Threads:", runtime.ThreadCreateProfile(nil))

// Статистика планировщика
var stats runtime.MemStats
runtime.ReadMemStats(&stats)

// GOMAXPROCS
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
}

10. Настройка GOMAXPROCS

func configureGOMAXPROCS() {
// По умолчанию = количество CPU
fmt.Println("Default GOMAXPROCS:", runtime.GOMAXPROCS(0))

// Установка вручную
runtime.GOMAXPROCS(4)

// Для CPU-bound задач: GOMAXPROCS = NumCPU
// Для I/O-bound задач: GOMAXPROCS может быть больше
// Для контейнеров: используйте automaxprocs
}

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

func performanceIssues() // Проблема 1: Слишком много горутин
func tooManyGoroutines() {
// Горутины дешёвые, но не бесплатные
// Слишком много горутин увеличивает нагрузку на планировщик

// Решение: используйте worker pools
}

// Проблема 2: Блокировка на каналах
func channelBlocking() {
// Горутины, заблокированные на каналах, не освобождают P
// Но M может быть освобождён при системных вызовах
}

// Проблема 3: False sharing
func falseSharing() {
// Данные, часто изменяемые разными горутинами,
// могут находиться в одной кэш-линии CPU
// Решение: выравнивание данных
}

12. Рекомендации

  • Используйте runtime.GOMAXPROCS(0) для получения количества CPU
  • Для CPU-bound задач устанавливайте GOMAXPROCS = NumCPU
  • Для I/O-bound задач можно увеличить GOMAXPROCS
  • Используйте automaxprocs для контейнеров
  • Мониторьте количество горутин и потоков ОС
  • Избегайте горутин с длительными вычислениями без точек вытеснения

Итог: Планировщик Go использует модель GMP (Goroutine, Machine, Processor). Каждый P имеет локальную очередь на 256 горутин. Work stealing позволяет балансировать нагрузку между P. Вытеснение горутин происходит по таймеру (~10 мс) или в точках вытеснения. Network poller обеспечивает неблокирующие сетевые операции.

Вопрос 25. Как работает garbage collector в Go?

Таймкод: 01:14:18

Ответ собеседника: Правильный. В Go используется трёхфазный mark-and-sweep сборщик мусора. На первом шаге все объекты считаются живыми (не подлежат удалению). На втором шаге помечаются корневые объекты (stack, globals). На третьем шаге от корней помечаются все достижимые объекты. Все непомеченные объекты удаляются. В Go GC нельзя настраивать вручную — разработчики решили убрать эту сложность у программистов. В C# есть поколенческий GC с более сложной логикой.

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

Garbage collector в Go — это один из наиболее оптимизированных компонентов runtime. Он использует конкурентный трицветный алгоритм mark-and-sweep с минимальными паузами (STW — Stop The World).

1. Алгоритм: трицветная маркировка

// Три цвета объектов:
// Белый — потенциально мусор (будет собран)
// Серый — достижим, но его потомки ещё не проверены
// Чёрный — достижим и все потомки проверены

// Фазы:
// 1. Все объекты белые
// 2. Корневые объекты (stack, globals) помечаются серыми
// 3. Серые объекты сканируются, их потомки помечаются серыми
// 4. Отсканированные серые объекты помечаются чёрными
// 5. Белые объекты собираются

2. Фазы работы GC

func gcPhases() {
// 1. STW (Stop The World) — подготовка
// - Включение write barrier
// - Сканирование стеков
// Длительность: ~10-100 мкс

// 2. Concurrent Mark — конкурентная маркировка
// - Пометка достижимых объектов
// - Выполняется параллельно с пользовательским кодом
// Длительность: зависит от количества объектов

// 3. Mark Termination — завершение маркировки
// - STW для завершения маркировки
// Длительность: ~10-100 мкс

// 4. Concurrent Sweep — конкурентная очистка
// - Освобождение памяти белых объектов
// - Выполняется параллельно с пользовательским кодом
}

3. Write Barrier

func writeBarrierExplanation() {
// Write barrier — механизм для корректной конкурентной маркировки

// Когда горутина изменяет указатель:
ptr = newObject

// Write barrier проверяет:
// 1. Если ptr указывает на белый объект — пометить его серым
// 2. Если старый объект был чёрным — пометить его серым

// Это предотвращает ситуацию, когда:
// - Горутина A читает чёрный объект
// - Горутина B записывает в него указатель на белый объект
// - Белый объект будет ошибочно собран
}

4. Настройка GC через GOGC

func configureGC() {
// GOGC устанавливает целевой процент роста кучи
// По умолчанию: GOGC=100

// GOGC=100 означает:
// Если куча была 100 МБ, следующий GC запустится при 200 МБ

// Установка через переменную окружения:
// GOGC=200 — реже запуск GC, больше потребление памяти
// GOGC=50 — чаще запуск GC, меньше потребление памяти

// Программная настройка (Go 1.19+)
debug.SetGCPercent(100)

// Принудительный запуск GC
runtime.GC()

// Отключение GC (не рекомендуется!)
debug.SetGCPercent(-1)
}

5. Мониторинг GC

func monitorGC() {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)

// Статистика кучи
fmt.Printf("HeapAlloc: %d MB\n", stats.HeapAlloc/1024/1024)
fmt.Printf("HeapSys: %d MB\n", stats.HeapSys/1024/1024)
fmt.Printf("HeapIdle: %d MB\n", stats.HeapIdle/1024/1024)
fmt.Printf("HeapInuse: %d MB\n", stats.HeapInuse/1024/1024)

// Статистика GC
fmt.Printf("NumGC: %d\n", stats.NumGC)
fmt.Printf("PauseTotalNs: %d ms\n", stats.PauseTotalNs/1e6)
fmt.Printf("LastGC: %v\n", time.Unix(0, int64(stats.LastGC)))

// Среднее время паузы
if stats.NumGC > 0 {
avgPause := time.Duration(stats.PauseTotalNs / uint64(stats.NumGC))
fmt.Printf("Avg GC pause: %v\n", avgPause)
}

// GCCPUFraction — доля CPU, потраченная на GC
fmt.Printf("GCCPUFraction: %.4f%%\n", stats.GCCPUFraction*100)
}

6. Использование pprof для анализа памяти

import _ "net/http/pprof"

func enablePprof() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}

// Анализ:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/allocs

7. Оптимизация работы с памятью

func memoryOptimization() {
// 1. Используйте sync.Pool для переиспользования объектов
var bufferPool = sync.Pool{
New: func() any {
return make([]byte, 4096)
},
}

func process() {
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf)
// Используем buf
}

// 2. Избегайте лишних аллокаций
// ПЛОХО
func bad() string {
s := ""
for i := 0; i < 1000; i++ {
s += fmt.Sprintf("%d", i) // Новая аллокация на каждой итерации
}
return s
}

// ХОРОШО
func good() string {
var b strings.Builder
b.Grow(4000) // Предварительное выделение
for i := 0; i < 1000; i++ {
fmt.Fprintf(&b, "%d", i)
}
return b.String()
}

// 3. Используйте массивы вместо слайсов, когда возможно
// Массив выделяется на стеке (если не убегает)
func stackArray() {
var arr [1000]int // На стеке
// ...
}
}

8. Escape Analysis

func escapeAnalysis() {
// Go анализирует, убегает ли переменная в кучу
// Если переменная не убегает — выделяется на стеке

// На стеке (не убегает)
func stackAlloc() int {
x := 42
return x // x не убегает
}

// В куче (убегает)
func heapAlloc() *int {
x := 42
return &x // x убегает через указатель
}

// Проверка escape analysis
// go build -gcflags="-m" main.go
}

9. Сравнение GC Go и C#

ХарактеристикаGoC#
АлгоритмConcurrent mark-and-sweepGenerational mark-and-sweep
ПоколенияНет (до Go 1.22)3 поколения (Gen 0, 1, 2)
Паузы~10-100 мксЗависит от поколения
НастройкаGOGCМного параметров
Размер кучиДинамическийФиксированный лимит

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

func gcPerformanceIssues() {
// Проблема 1: Слишком много короткоживущих объектов
func shortLivedObjects() {
for i := 0; i < 1000000; i++ {
_ = make([]byte, 1024) // Аллокация на каждой итерации
}
}

// Решение: переиспользование через sync.Pool

// Проблема 2: Большие объекты
func largeObjects() {
data := make([]byte, 100*1024*1024) // 100 МБ
// Увеличивает время маркировки
}

// Решение: разбить на части или использовать mmap

// Проблема 3: Утечки памяти через горутины
func goroutineLeak() {
ch := make(chan int)
go func() {
<-ch // Заблокируется навсегда
// Все локальные переменные горутины утекут
}()
}
}

11. Рекомендации

  • Используйте GOGC для настройки частоты GC
  • Мониторьте GCCPUFraction — должно быть < 5%
  • Используйте sync.Pool для переиспользования объектов
  • Избегайте лишних аллокаций в горячих путях
  • Используйте pprof для анализа утечек памяти
  • Проверяйте escape analysis через -gcflags="-m"

Итог: GC в Go — конкурентный трицветный mark-and-sweep с минимальными паузами. Настраивается через GOGC. Используйте sync.Pool, избегайте лишних аллокаций, мониторьте через pprof. Среднее время паузы GC — десятки микросекунд.

Вопрос 26. Что выведет код с кастомной ошибкой и почему?

Таймкод: 01:16:43

Ответ собеседника: Правильный. Код выведет пустую строку. Причина: кастомная ошибка реализует метод Error(), который всегда возвращает константную строку. При присваивании err := &CustomError{} переменная err имеет тип *CustomError, но при использовании в fmt.Println вызывается метод Error(), который возвращает пустую строку. Это демонстрация того, что кастомные ошибки могут хранить дополнительные поля для контекста, но в данном примере они не заполнены.

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

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

1. Проблемный код

type CustomError struct {
Code int
Message string
}

func (e *CustomError) Error() string {
return "" // Всегда возвращает пустую строку!
}

func main() {
err := &CustomError{Code: 404, Message: "Not Found"}
fmt.Println(err) // Выведет: пустая строка
}

2. Почему выводится пустая строка

func explanation() {
// Когда fmt.Println получает значение, реализующее error:
// 1. Проверяет, реализует ли тип интерфейс error
// 2. Если да — вызывает метод Error()
// 3. Выводит результат Error()

err := &CustomError{Code: 404, Message: "Not Found"}

// fmt.Println вызывает:
fmt.Println(err.Error()) // ""

// Поля Code и Message существуют, но не используются в Error()
fmt.Println(err.Code) // 404
fmt.Println(err.Message) // "Not Found"
}

3. Правильная реализация кастомной ошибки

// Вариант 1: Использование полей в Error()
type CustomError struct {
Code int
Message string
}

func (e *CustomError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}

func main() {
err := &CustomError{Code: 404, Message: "Not Found"}
fmt.Println(err) // error 404: Not Found
}

4. Паттерн: ошибка с контекстом

type AppError struct {
Code int
Message string
Err error // Вложенная ошибка
StackTrace string // Стек вызовов
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Распаковка вложенной ошибки
func (e *AppError) Unwrap() error {
return e.Err
}

func main() {
// Создание цепочки ошибок
dbErr := fmt.Errorf("connection refused")
appErr := &AppError{
Code: 500,
Message: "Database error",
Err: dbErr,
}

fmt.Println(appErr) // [500] Database error: connection refused

// Проверка типа
var target *AppError
if errors.As(appErr, &target) {
fmt.Println("Code:", target.Code) // 500
}

// Проверка вложенной ошибки
if errors.Is(appErr, dbErr) {
fmt.Println("Original error found")
}
}

5. Паттерн: ошибка с дополнительными данными

type ValidationError struct {
Field string
Value any
Rule string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: field %q with value %v does not satisfy rule %q",
e.Field, e.Value, e.Rule)
}

func (e *ValidationError) Unwrap() error {
return ErrValidation
}

var ErrValidation = errors.New("validation error")

func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Rule: "0 <= age <= 150",
}
}
return nil
}

func main() {
err := validateAge(-5)
fmt.Println(err)
// validation failed: field "age" with value -5 does not satisfy rule "0 <= age <= 150"

// Проверка типа
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Invalid field: %s\n", valErr.Field)
}
}

6. Паттерн: ошибка с форматированием (Go 1.13+)

type NotFoundError struct {
Resource string
ID int64
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}

func (e *NotFoundError) Is(target error) bool {
_, ok := target.(*NotFoundError)
return ok
}

func findUser(id int64) (*User, error) {
// Имитация поиска
return nil, &NotFoundError{Resource: "User", ID: id}
}

func main() {
_, err := findUser(42)

// Проверка через errors.Is
var notFound *NotFoundError
if errors.As(err, &notFound) {
fmt.Printf("Not found: %s %d\n", notFound.Resource, notFound.ID)
}
}

7. Паттерн: ошибка с кодом статуса

type HTTPError struct {
StatusCode int
Message string
}

func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}

func (e *HTTPError) Is(target error) bool {
t, ok := target.(*HTTPError)
if !ok {
return false
}
return e.StatusCode == t.StatusCode
}

func handleRequest() error {
return &HTTPError{
StatusCode: 401,
Message: "Unauthorized",
}
}

func main() {
err := handleRequest()

// Проверка кода статуса
var httpErr *HTTPError
if errors.As(err, &httpErr) {
switch httpErr.StatusCode {
case 401:
fmt.Println("Redirect to login")
case 404:
fmt.Println("Show not found page")
case 500:
fmt.Println("Show error page")
}
}
}

8. Паттерн: коллекция ошибок

type ValidationErrors []error

func (ve ValidationErrors) Error() string {
var b strings.Builder
b.WriteString("validation errors:")
for _, err := range ve {
b.WriteString("\n - ")
b.WriteString(err.Error())
}
return b.String()
}

func (ve *ValidationErrors) Add(err error) {
*ve = append(*ve, err)
}

func (ve ValidationErrors) HasErrors() bool {
return len(ve) > 0
}

func validateUser(user *User) error {
var errs ValidationErrors

if user.Name == "" {
errs.Add(&ValidationError{Field: "name", Rule: "required"})
}
if user.Age < 0 {
errs.Add(&ValidationError{Field: "age", Rule: "must be positive"})
}
if user.Email == "" {
errs.Add(&ValidationError{Field: "email", Rule: "required"})
}

if errs.HasErrors() {
return errs
}
return nil
}

9. Использование errors.Is и errors.As

func errorChecking() {
// errors.Is — проверка на конкретную ошибку
err := fmt.Errorf("wrapped: %w", ErrNotFound)

if errors.Is(err, ErrNotFound) {
fmt.Println("Not found error detected")
}

// errors.As — проверка на тип ошибки
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Println("App error code:", appErr.Code)
}
}

10. Рекомендации

  • Всегда используйте поля структуры в методе Error()
  • Реализуйте Unwrap() для цепочек ошибок
  • Используйте errors.Is и errors.As для проверки ошибок
  • Создавайте специфичные типы ошибок для разных ситуаций
  • Храните контекст в полях структуры ошибки

Итог: Кастомная ошибка с пустым методом Error() выводит пустую строку. Правильная реализация использует поля структуры для информативного сообщения. Используйте errors.Is, errors.As и Unwrap() для работы с цепочками ошибок.

Вопрос 27. Что выведет код с проверкой ошибок на nil в разных сценариях (var error, &CustomError{}, CustomError{})?

Таймкод: 01:18:15

Ответ собеседника: Правильный. 1) var err error → nil, проверка err == nil = true (интерфейс не инициализирован). 2) var err *CustomError (указатель) → указатель на структуру, но без инициализации полей, err == nil = false (указатель не nil, память выделена). 3) err := &CustomError{} → указатель на инициализированную структуру, err == nil = false. 4) err := CustomError{} → структура по значению, err == nil = false. Особенность: указатель на интерфейс без инициализации — это nil, но указатель на структуру — не nil, даже если поля пустые.

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

Это одна из самых коварных ловушек в Go, связанная с внутренним устройством интерфейсов и тем, как они сравниваются с nil.

1. Внутреннее устройство интерфейса

// Интерфейс — это пара указателей
type eface struct {
_type *_type // информация о типe
data unsafe.Pointer // указатель на данные
}

// Интерфейс равен nil, только когда ОБА поля равны nil

2. Различные сценарии

type CustomError struct {
Code int
Message string
}

func (e *CustomError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}

func demonstrateScenarios() {
// Сценарий 1: var err error
var err error
fmt.Println("var err error:", err == nil) // true
// Интерфейс полностью не инициализирован: type=nil, data=nil

// Сценарий 2: var err *CustomError (указатель)
var err2 *CustomError
fmt.Println("var err *CustomError:", err2 == nil) // true
// Указатель не инициализирован — это nil

// Сценарий 3: err := &CustomError{}
err3 := &CustomError{}
fmt.Println("err := &CustomError{}:", err3 == nil) // false
// Указатель инициализирован — указывает на структуру

// Сценарий 4: err := CustomError{}
err4 := CustomError{}
fmt.Println("err := CustomError{}:", err4 == nil) // Ошибка компиляции!
// Структура не реализует error (только *CustomError)

// Сценарий 5: Коварный случай — интерфейс с типом, но без данных
var err5 *CustomError
var err error = err5
fmt.Println("var err error = (*CustomError)(nil):", err == nil) // false!
// Интерфейс содержит тип *CustomError, но data=nil
// Это НЕ nil интерфейс!
}

3. Коварный случай: nil указатель в интерфейсе

func dangerousCase() {
// Функция возвращает *CustomError
var err *CustomError = nil

// Присваиваем интерфейсу
var e error = err

// e НЕ равен nil!
if e == nil {
fmt.Println("This won't print") // Не выполнится
} else {
fmt.Println("e is not nil!") // Выполнится
}

// Но вызов Error() вызовет panic!
// fmt.Println(e.Error()) // panic: runtime error: invalid memory address
}

4. Реальная проблема в коде

type Service struct{}

func (s *Service) FindUser(id int) (*User, error) {
// Имитация: пользователь не найден
return nil, nil // Возвращаем nil ошибку
}

func (s *Service) ProcessUser(id int) error {
user, err := s.FindUser(id)
if err != nil {
return fmt.Errorf("failed to find user: %w", err)
}
// Обработка пользователя
return nil
}

// Проблема: если FindUser вернёт *CustomError(nil)
func (s *Service) FindUserV2(id int) (*User, error) {
var err *CustomError // nil указатель
return nil, err // Возвращаем nil указатель как error
}

func main() {
svc := &Service{}

// Работает корректно
err := svc.ProcessUser(1)
if err != nil {
fmt.Println("Error:", err)
}

// Проблема!
_, err = svc.FindUserV2(1)
if err != nil {
fmt.Println("This will print!") // Выполнится, хотя ошибки нет!
}
}

5. Правильный паттерн возврата ошибок

// ПРАВИЛЬНО: Возвращайте явный nil
func goodFunction() error {
// Если ошибки нет
return nil // Возвращаем nil интерфейс, а не nil указатель
}

// ПРАВИЛЬНО: Используйте переменную ошибки
func goodFunction2() error {
var err error // nil интерфейс

// Логика...
if somethingWrong {
err = &CustomError{Code: 500, Message: "error"}
}

return err // Либо nil, либо конкретная ошибка
}

// ПРАВИЛЬНО: Создавайте ошибку при возврате
func goodFunction3() error {
if somethingWrong {
return &CustomError{Code: 500, Message: "error"}
}
return nil
}

6. Паттерn: проверка ошибок

func properErrorChecking() {
err := doSomething()

// Проверка на nil
if err != nil {
// Обработка ошибки

// Проверка типа
var customErr *CustomError
if errors.As(err, &customErr) {
fmt.Println("Custom error code:", customErr.Code)
}

// Проверка конкретной ошибки
if errors.Is(err, ErrNotFound) {
fmt.Println("Not found")
}
}
}

7. Паттерn: Sentinel errors

// Определение sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrValidation = errors.New("validation failed")
ErrInternal = errors.New("internal error")
)

func findUser(id int) (*User, error) {
// Имитация: пользователь не найден
return nil, ErrNotFound
}

func main() {
_, err := findUser(1)

// Проверка через errors.Is
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
}

8. Паттерn: обёрнутые ошибки

func wrappedErrors() {
// Создание цепочки ошибок
original := errors.New("original error")
wrapped := fmt.Errorf("context: %w", original)
doubleWrapped := fmt.Errorf("more context: %w", wrapped)

fmt.Println(doubleWrapped)
// more context: context: original error

// Проверка оригинальной ошибки
if errors.Is(doubleWrapped, original) {
fmt.Println("Found original error") // Выполнится
}

// Распаковка
fmt.Println(errors.Unwrap(doubleWrapped)) // context: original error
}

9. Таблица сравнения с nil

ОбъявлениеТип== nil?Комментарий
var err errorinterfacetrueНе инициализирован
var err *CustomError*CustomErrortrueУказатель nil
err := &CustomError{}*CustomErrorfalseУказатель на структуру
var e error = (*CustomError)(nil)interfacefalseТип есть, данных нет
var e error = nilinterfacetrueПолный nil

10. Рекомендации

  • Всегда возвращайте nil для ошибок, а не nil-указатель
  • Используйте errors.Is и errors.As для проверки ошибок
  • Не сравнивайте ошибки через ==, кроме sentinel errors
  • Используйте fmt.Errorf с %w для обёртывания ошибок
  • Создавайте кастомные типы ошибок для специфичных случаев

Итог: Интерфейс равен nil только когда и тип, и данные равны nil. Nil-указатель, присвоенный интерфейсу, делает его не-nil. Это приводит к коварным багам. Всегда возвращайте явный nil для ошибок, а не nil-указатель конкретного типа.

Вопрос 28. Как правильно оборачивать ошибки в Go и в чём разница между fmt.Errorf с %w и структурной обёрткой?

Таймкод: 01:26:55

Ответ собеседника: Правильный. Есть два способа: 1) fmt.Errorf("context: %w", err) — создаёт текстовую обёртку, можно достать через errors.As и errors.Is. 2) Структурная обёртка — кастомная структура с полем error, позволяющая хранить дополнительный контекст (ID, параметры). Структурный способ удобнее для сохранения типизированной информации на каждом уровне вызова, а не только текста.

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

Правильное обёртывание ошибок — это искусство, которое делает код поддерживаемым и отладку простой. Go предлагает несколько подходов, каждый из которых имеет свои преимущества.

1. fmt.Errorf с %w — текстовая обёртка

func textWrapping() {
// Базовый слой
original := errors.New("connection refused")

// Обёртка с контекстом
wrapped := fmt.Errorf("failed to connect to database: %w", original)

// Ещё один уровень
doubleWrapped := fmt.Errorf("user service initialization failed: %w", wrapped)

fmt.Println(doubleWrapped)
// user service initialization failed: failed to connect to database: connection refused

// Проверка через errors.Is
if errors.Is(doubleWrapped, original) {
fmt.Println("Found original error") // Выполнится
}

// Распаковка через errors.Unwrap
fmt.Println(errors.Unwrap(doubleWrapped))
// failed to connect to database: connection refused
}

2. Структурная обёртка

// Кастомная структура ошибки
type AppError struct {
Op string // операция, которая вызвала ошибку
Code int // код ошибки
Err error // вложенная ошибка
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Op, e.Err)
}
return e.Op
}

func (e *AppError) Unwrap() error {
return e.Err
}

// Конструктор для удобства
func NewAppError(op string, code int, err error) *AppError {
return &AppError{
Op: op,
Code: code,
Err: err,
}
}

// Использование
func structuralWrapping() {
// Базовый слой
original := errors.New("connection refused")

// Обёртка с контекстом
dbErr := NewAppError("db.Connect", 500, original)

// Ещё один уровень
svcErr := NewAppError("userService.Init", 503, dbErr)

fmt.Println(svcErr)
// userService.Init: db.Connect: connection refused

// Проверка через errors.As
var appErr *AppError
if errors.As(svcErr, &appErr) {
fmt.Println("Operation:", appErr.Op) // db.Connect
fmt.Println("Code:", appErr.Code) // 500
}

// Проверка оригинальной ошибки
if errors.Is(svcErr, original) {
fmt.Println("Found original error") // Выполнится
}
}

3. Сравнение подходов

func comparison() {
// Текстовая обёртка
original := errors.New("not found")
textErr := fmt.Errorf("user with ID 42: %w", original)

// Плюсы:
// - Простота
// - Работает с errors.Is
// - Читаемый вывод

// Минусы:
// - Нельзя извлечь типизированные данные
// - Парсинг строки для получения контекста

// Структурная обёртка
structErr := &AppError{
Op: "GetUser",
Code: 404,
Err: original,
}

// Плюсы:
// - Типизированные данные
// - Программный доступ к полям
// - Работает с errors.As

// Минусы:
// - Больше кода
// - Нужно определять структуру
}

4. Паттерn: ошибка с полным контекстом

type DetailedError struct {
Op string // операция
Code int // код ошибки
Time time.Time // время возникновения
UserID int64 // ID пользователя
Resource string // ресурс
Err error // вложенная ошибка
}

func (e *DetailedError) Error() string {
return fmt.Sprintf("[%s] %s (code=%d, user=%d, resource=%s): %v",
e.Time.Format(time.RFC3339), e.Op, e.Code, e.UserID, e.Resource, e.Err)
}

func (e *DetailedError) Unwrap() error {
return e.Err
}

func findUser(ctx context.Context, id int64) (*User, error) {
// Имитация ошибки
original := errors.New("connection timeout")

return nil, &DetailedError{
Op: "FindUser",
Code: 500,
Time: time.Now(),
UserID: id,
Resource: "users",
Err: original,
}
}

func main() {
_, err := findUser(context.Background(), 42)

// Извлечение контекста
var detailed *DetailedError
if errors.As(err, &detailed) {
fmt.Printf("Operation: %s\n", detailed.Op)
fmt.Printf("User ID: %d\n", detailed.UserID)
fmt.Printf("Resource: %s\n", detailed.Resource)
fmt.Printf("Time: %v\n", detailed.Time)
}
}

5. Паттерn: цепочка ошибок с типизацией

// Базовый тип ошибки приложения
type AppErrorLayer int

const (
LayerTransport AppErrorLayer = iota
LayerService
LayerRepository
LayerDatabase
)

type LayeredError struct {
Layer AppErrorLayer
Op string
Code int
Message string
Err error
}

func (e *LayeredError) Error() string {
layers := []string{"Transport", "Service", "Repository", "Database"}
layerName := layers[e.Layer]

if e.Err != nil {
return fmt.Sprintf("[%s] %s (code=%d): %v", layerName, e.Op, e.Code, e.Err)
}
return fmt.Sprintf("[%s] %s (code=%d)", layerName, e.Op, e.Code)
}

func (e *LayeredError) Unwrap() error {
return e.Err
}

func handleRequest() error {
// Repository layer
dbErr := &LayeredError{
Layer: LayerDatabase,
Op: "Query",
Code: 500,
Message: "connection refused",
}

// Service layer
svcErr := &LayeredError{
Layer: LayerService,
Op: "GetUser",
Code: 503,
Err: dbErr,
}

// Transport layer
transportErr := &LayeredError{
Layer: LayerTransport,
Op: "HandleRequest",
Code: 500,
Err: svcErr,
}

return transportErr
}

func main() {
err := handleRequest()

// Обход цепочки ошибок
for err != nil {
var layered *LayeredError
if errors.As(err, &layered) {
fmt.Printf("Layer: %d, Op: %s, Code: %d\n",
layered.Layer, layered.Op, layered.Code)
}
err = errors.Unwrap(err)
}
}

6. Паттерn: ошибка с параметрами запроса

type RequestError struct {
Method string
URL string
Status int
Response string
Err error
}

func (e *RequestError) Error() string {
return fmt.Sprintf("HTTP %s %s failed with status %d: %v",
e.Method, e.URL, e.Status, e.Err)
}

func (e *RequestError) Unwrap() error {
return e.Err
}

func makeRequest(method, url string) error {
// Имитация HTTP запроса
resp, err := http.Get(url)
if err != nil {
return &RequestError{
Method: method,
URL: url,
Err: err,
}
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
return &RequestError{
Method: method,
URL: url,
Status: resp.StatusCode,
Err: fmt.Errorf("HTTP %d", resp.StatusCode),
}
}

return nil
}

func main() {
err := makeRequest("GET", "https://api.example.com/users")

var reqErr *RequestError
if errors.As(err, &reqErr) {
fmt.Printf("Request failed: %s %s\n", reqErr.Method, reqErr.URL)
if reqErr.Status > 0 {
fmt.Printf("Status: %d\n", reqErr.Status)
}
}
}

7. Комбинированный подход

// Используйте структурные ошибки для типизации
// и fmt.Errorf для быстрого добавления контекста

func combinedApproach() {
// Структурная ошибка для типизированных данных
dbErr := &AppError{
Op: "db.Query",
Code: 500,
Err: errors.New("connection refused"),
}

// Текстовая обёртка для быстрого контекста
svcErr := fmt.Errorf("failed to get user %d: %w", 42, dbErr)

// Снова структурная ошибка
transportErr := &AppError{
Op: "http.Handle",
Code: 503,
Err: svcErr,
}

fmt.Println(transportErr)
// http.Handle: failed to get user 42: db.Query: connection refused

// Проверка работает на всю цепочку
if errors.Is(transportErr, dbErr) {
fmt.Println("Found db error")
}
}

8. Рекомендации

  • Используйте fmt.Errorf с %w для простого добавления контекста
  • Используйте структурные ошибки, когда нужны типизированные данные
  • Всегда реализуйте Unwrap() для структурных ошибок
  • Используйте errors.Is и errors.As для проверки ошибок
  • Не сравнивайте ошибки через == (кроме sentinel errors)

Итог: fmt.Errorf с %w — для простого текстового контекста, структурные ошибки — для типизированных данных. Оба подхода работают с errors.Is и errors.As. Комбинируйте их в зависимости от потребностей.

Вопрос 29. Для чего используется контекст (context) в Go и какие возможности он предоставляет?

Таймкод: 01:32:34

Ответ собеседника: Правильный. Контекст — это структура данных для управления жизненным циклом операций. Основные возможности: 1) Каскадная отмена — можно создать дочерний контекст и при отмене родителя все дочерние тоже отменяются. 2) Тайм-ауты и дедлайны — можно ограничить время выполнения конкретной операции, не затрагивая другие. 3) Передача значений — можно хранить в контексте данные (например, request ID), но не рекомендуется злоупотреблять этим — лучше передавать параметры явно через структуры.

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

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

1. Основные возможности context

func contextCapabilities() {
// 1. Отмена (cancellation)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// 2. Тайм-аут (timeout)
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 3. Дедлайн (deadline)
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
defer cancel()

// 4. Значения (values)
ctx = context.WithValue(ctx, "requestID", "12345")
}

2. Каскадная отмена

func cascadeCancellation() {
// Создаём корневой контекст
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()

// Создаём дочерние контексты
childCtx1, childCancel1 := context.WithCancel(rootCtx)
defer childCancel1()

childCtx2, childCancel2 := context.WithCancel(rootCtx)
defer childCancel2()

// Запускаем горутины с дочерними контекстами
go func() {
<-childCtx1.Done()
fmt.Println("Child 1 cancelled:", childCtx1.Err())
}()

go func() {
<-childCtx2.Done()
fmt.Println("Child 2 cancelled:", childCtx2.Err())
}()

// Отмена корневого контекста отменяет все дочерние
time.Sleep(100 * time.Millisecond)
rootCancel()

time.Sleep(100 * time.Millisecond)
// Child 1 cancelled: context canceled
// Child 2 cancelled: context canceled
}

3. Тайм-ауты и дедлайны

func timeoutAndDeadline() {
// Тайм-аут — относительное время
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

// Дедлайн — абсолютное время
ctx2, cancel2 := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
defer cancel2()

// Проверка дедлайна
if deadline, ok := ctx.Deadline(); ok {
fmt.Println("Deadline:", deadline)
}

// Использование в горутине
go func() {
select {
case <-time.After(5 * time.Second):
fmt.Println("Work completed")
case <-ctx.Done():
fmt.Println("Cancelled:", ctx.Err()) // context deadline exceeded
}
}()
}

4. Передача значений

// Определение типа ключа (лучшая практика)
type contextKey string

const (
RequestIDKey contextKey = "requestID"
UserIDKey contextKey = "userID"
)

func withValues() {
ctx := context.Background()

// Добавление значений
ctx = context.WithValue(ctx, RequestIDKey, "req-12345")
ctx = context.WithValue(ctx, UserIDKey, int64(42))

// Получение значений
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
fmt.Println("Request ID:", reqID)
}

if userID, ok := ctx.Value(UserIDKey).(int64); ok {
fmt.Println("User ID:", userID)
}
}

5. Использование в HTTP-сервере

func httpServerExample() {
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// Контекст запроса
ctx := r.Context()

// Добавляем значения
ctx = context.WithValue(ctx, RequestIDKey, generateRequestID())

// Вызываем бизнес-логику
user, err := getUser(ctx, 42)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(user)
})

http.ListenAndServe(":8080", nil)
}

func getUser(ctx context.Context, id int64) (*User, error) {
// Проверяем отмену
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}

// Имитация запроса к БД
time.Sleep(100 * time.Millisecond)
return &User{ID: id, Name: "John"}, nil
}

6. Использование с базой данных

func databaseExample() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Запрос с контекстом
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", 42)

var name string
if err := row.Scan(&name); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Query timed out")
}
return
}

fmt.Println("Name:", name)
}

7. Использование с HTTP-клиентом

func httpClientExample() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/users", nil)
if err != nil {
log.Fatal(err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out")
}
return
}
defer resp.Body.Close()

// Обработка ответа
}

8. Паттерn: worker с контекстом

func workerWithContext(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker shutting down:", ctx.Err())
return
case job, ok := <-jobs:
if !ok {
fmt.Println("Jobs channel closed")
return
}
processJob(ctx, job)
}
}
}

func processJob(ctx context.Context, job Job) {
// Передаём контекст вниз по цепочке вызовов
result, err := callExternalService(ctx, job)
if err != nil {
fmt.Println("Job failed:", err)
return
}

if err := saveResult(ctx, result); err != nil {
fmt.Println("Failed to save result:", err)
}
}

9. Паттерn: graceful shutdown

func gracefulShutdown() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Обработка сигналов
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// Запуск сервера
srv := &http.Server{Addr: ":8080"}

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

// Ожидание сигнала
<-sigChan
fmt.Println("Shutting down...")

// Создаём контекст с таймаутом для shutdown
shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 30*time.Second)
defer shutdownCancel()

// Graceful shutdown сервера
if err := srv.Shutdown(shutdownCtx); err != nil {
fmt.Println("Shutdown error:", err)
}

fmt.Println("Server stopped")
}

10. Паттерn: пайплайн с контекстом

func pipelineWithContext() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// Генератор
generate := func(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 100; i++ {
select {
case ch <- i:
case <-ctx.Done():
return
}
}
}()
return ch
}

// Обработчик
process := func(ctx context.Context, in <-chan int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for val := range in {
select {
case ch <- val * 2:
case <-ctx.Done():
return
}
}
}()
return ch
}

// Пайплайн
for result := range process(ctx, process(ctx, generate(ctx))) {
fmt.Println(result)
}
}

11. Антипаттерны

func antiPatterns() {
// АНТИПАТТЕРН 1: Хранение большого количества данных в контексте
ctx := context.Background()
ctx = context.WithValue(ctx, "user", &User{Name: "John", Email: "john@example.com"})
ctx = context.WithValue(ctx, "config", &Config{/* много полей */})
// Лучше: передавать явно через параметры

// АНТИПАТТЕРН 2: Не вызывать cancel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel() // Забыли! Утечка ресурсов

// АНТИПАТТЕРН 3: Использование context.Background() вместо передачи контекста
func badFunction() {
ctx := context.Background() // Неправильно!
// Лучше: принимать ctx context.Context как параметр
}

// АНТИПАТТЕРН 4: Использование string как ключа
ctx = context.WithValue(ctx, "requestID", "123") // Может быть конфликт!
// Лучше: использовать пользовательский тип
}

12. Рекомендации

  • Передавайте context.Context как первый параметр функции
  • Всегда вызывайте cancel() через defer
  • Используйте пользовательские типы для ключей значений
  • Не храните в контексте большие объекты
  • Используйте контекст для отмены, а не для передачи параметров

Итог: context — стандартный механизм для управления жизненным циклом операций. Основные возможности: каскадная отмена, тайм-ауты/дедлайны, передача значений. Передавайте контекст как первый параметр, всегда вызывайте cancel(), используйте пользовательские типы для ключей.

Вопрос 30. Как организовано тестирование в команде и какие типы тестов используете?

Таймкод: 01:36:05

Ответ собеседника: Правильный. Используется стандартный пакет testing. Юнит-тесты пишутся для каждого слоя отдельно с моками интерфейсов. Покрытие стараются доводить до 70-80% для критичных сервисов. Интеграционные тесты пишутся по зелёным сценариям — проверяют взаимодействие всех слоёв, запись в базу данных. Для рефакторинга критичных сервисов интеграционные тесты обязательны. Бенчмарки использовались для сравнения производительности стандартного JSON и альтернативных библиотек (в 10 раз быстрее).

Правильный 答案:

Тестирование — это неотъемлемая часть разработки. Правильная организация тестов позволяет быстро находить баги, безопасно рефакторить код и уверенно выпускать релизы.

1. Пирамида тестирования

// Пирамида тестирования:
//
// / E2E \ <- мало, медленные, хрупкие
// / Integration \ <- среднее количество
// / Unit Tests \ <- много, быстрые, стабильные
// /________________\

2. Юнит-тесты

// user_service_test.go
package service

import (
"context"
"errors"
"testing"
)

// Мок репозитория
type mockUserRepo struct {
users map[int64]*User
err error
}

func (m *mockUserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
if m.err != nil {
return nil, m.err
}
user, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}

func (m *mockUserRepo) Save(ctx context.Context, user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}

// Тесты
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID int64
mockUsers map[int64]*User
mockErr error
wantUser *User
wantErr error
}{
{
name: "existing user",
userID: 1,
mockUsers: map[int64]*User{
1: {ID: 1, Name: "John", Email: "john@example.com"},
},
wantUser: &User{ID: 1, Name: "John", Email: "john@example.com"},
},
{
name: "user not found",
userID: 999,
mockUsers: map[int64]*User{},
wantErr: ErrNotFound,
},
{
name: "repository error",
userID: 1,
mockErr: errors.New("connection refused"),
wantErr: ErrInternal,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &mockUserRepo{
users: tt.mockUsers,
err: tt.mockErr,
}
svc := NewUserService(repo)

user, err := svc.GetUser(context.Background(), tt.userID)

if !errors.Is(err, tt.wantErr) {
t.Errorf("GetUser() error = %v, want %v", err, tt.wantErr)
return
}

if tt.wantUser != nil {
if user.ID != tt.wantUser.ID {
t.Errorf("GetUser() ID = %d, want %d", user.ID, tt.wantUser.ID)
}
if user.Name != tt.wantUser.Name {
t.Errorf("GetUser() Name = %s, want %s", user.Name, tt.wantUser.Name)
}
}
})
}
}

3. Табличные тесты

func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "user@example.com", false},
{"valid with plus", "user+tag@example.com", false},
{"missing @", "userexample.com", true},
{"missing domain", "user@", true},
{"empty", "", true},
{"spaces", "user @example.com", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
}
})
}
}

4. Интеграционные тесты

// user_repo_integration_test.go
//go:build integration

package repository

import (
"context"
"database/sql"
"testing"

_ "github.com/lib/pq"
"github.com/stretchr/testify/require"
)

func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("postgres", "postgres://test:test@localhost:5432/testdb?sslmode=disable")
require.NoError(t, err)

// Очистка таблицы перед тестом
_, err = db.Exec("TRUNCATE TABLE users RESTART IDENTITY CASCADE")
require.NoError(t, err)

t.Cleanup(func() {
db.Close()
})

return db
}

func TestUserRepo_FindByID_Integration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}

db := setupTestDB(t)
repo := NewPostgresUserRepo(db)
ctx := context.Background()

// Создаём пользователя
user := &User{
Name: "Test User",
Email: "test@example.com",
}
err := repo.Save(ctx, user)
require.NoError(t, err)
require.NotZero(t, user.ID)

// Читаем пользователя
found, err := repo.FindByID(ctx, user.ID)
require.NoError(t, err)
require.Equal(t, user.Name, found.Name)
require.Equal(t, user.Email, found.Email)

// Пытаемся найти несуществующего
_, err = repo.FindByID(ctx, 99999)
require.ErrorIs(t, err, ErrNotFound)
}

5. Бенчмарки

// json_benchmark_test.go
package main

import (
"encoding/json"
"testing"

jsoniter "github.com/json-iterator/go"
)

type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

func BenchmarkStdJSONMarshal(b *testing.B) {
user := User{ID: 1, Name: "John", Email: "john@example.com"}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(user)
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkJsoniterMarshal(b *testing.B) {
user := User{ID: 1, Name: "John", Email: "john@example.com"}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := jsoniter.Marshal(user)
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkStdJSONUnmarshal(b *testing.B) {
data := []byte(`{"id":1,"name":"John","email":"john@example.com"}`)

b.ResetTimer()
for i := 0; i < b.N; i++ {
var user User
err := json.Unmarshal(data, &user)
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkJsoniterUnmarshal(b *testing.B) {
data := []byte(`{"id":1,"name":"John","email":"john@example.com"}`)

b.ResetTimer()
for i := 0; i < b.N; i++ {
var user User
err := jsoniter.Unmarshal(data, &user)
if err != nil {
b.Fatal(err)
}
}
}

6. Запуск тестов

# Запуск всех тестов
go test ./...

# Запуск с покрытием
go test -cover ./...

# Запуск с детальным покрытием
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Запуск только юнит-тестов (без интеграционных)
go test -short ./...

# Запуск интеграционных тестов
go test -tags=integration ./...

# Запуск бенчмарков
go test -bench=. -benchmem ./...

# Запуск конкретного теста
go test -run TestUserService_GetUser ./...

# Запуск с флагом для обнаружения гонок
go test -race ./...

7. Организация тестов

project/
├── internal/
│ ├── service/
│ │ ├── user_service.go
│ │ ├── user_service_test.go # Юнит-тесты
│ │ └── user_service_benchmark_test.go # Бенчмарки
│ ├── repository/
│ │ ├── user_repo.go
│ │ ├── user_repo_test.go # Юнит-тесты с моками
│ │ └── user_repo_integration_test.go # Интеграционные тесты
│ └── handler/
│ ├── user_handler.go
│ └── user_handler_test.go # Юнит-тесты
└── test/
├── fixtures/ # Тестовые данные
└── helpers/ # Вспомогательные функции

8. Test helpers

// test/helpers/testdb.go
package helpers

import (
"database/sql"
"testing"

_ "github.com/lib/pq"
)

func SetupTestDB(t *testing.T) *sql.DB {
t.Helper()

db, err := sql.Open("postgres", "postgres://test:test@localhost:5432/testdb?sslmode=disable")
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}

t.Cleanup(func() {
db.Close()
})

return db
}

// test/helpers/factory.go
package helpers

func CreateTestUser(t *testing.T, db *sql.DB) *User {
t.Helper()

user := &User{
Name: "Test User",
Email: "test@example.com",
}

// Сохраняем в БД
// ...

return user
}

9. Покрытие кода

# Генерация отчёта о покрытии
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

# Пример вывода:
# internal/service/user_service.go:15: GetUser 100.0%
# internal/service/user_service.go:25: CreateUser 85.7%
# internal/service/user_service.go:40: DeleteUser 66.7%
# total: (statements) 82.4%

10. Рекомендации

  • Пишите юнит-тесты для всей бизнес-логики
  • Используйте табличные тесты для параметризованных тестов
  • Используйте моки для изоляции слоёв
  • Пишите интеграционные тесты для критичных сценариев
  • Используйте бенчмарки для сравнения производительности
  • Стремитесь к покрытию 70-80% для критичного кода
  • Запускайте тесты с -race для обнаружения гонок

Итог: Используйте пирамиду тестирования: много юнит-тестов, среднее количество интеграционных, мало E2E. Применяйте табличные тесты, моки, бенчмарки. Стремитесь к покрытию 70-80% для критичного кода. Запускайте тесты с -race для обнаружения гонок.

Вопрос 31. Используете ли вы профилирование (pprof) и табличные тесты?

Таймкод: 01:41:30

Ответ собеседника: Правильный. Табличные тесты (table-driven tests) рассматривали, но пока не внедрили — неудобно инициализировать много тестовых кейсов. Test suite тоже рассматривали для удобной инициализации и очистки. Профилирование через pprof пока не использовали, так как не сталкивались с проблемами производительности, требующими глубокого анализа. Бенчмарки писали для сравнения библиотек сериализации JSON.

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

Табличные тесты и профилирование — это мощные инструменты, которые стоит освоить. Давайте разберём оба подробно.

1. Табличные тесты (Table-Driven Tests)

// Базовый пример
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
price float64
discount float64
want float64
wantErr bool
}{
{
name: "10% discount",
price: 100,
discount: 10,
want: 90,
},
{
name: "0% discount",
price: 100,
discount: 0,
want: 100,
},
{
name: "100% discount",
price: 100,
discount: 100,
want: 0,
},
{
name: "negative discount",
price: 100,
discount: -10,
wantErr: true,
},
{
name: "discount over 100%",
price: 100,
discount: 150,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalculateDiscount(tt.price, tt.discount)

if (err != nil) != tt.wantErr {
t.Errorf("CalculateDiscount() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && got != tt.want {
t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
}
})
}
}

2. Табличные тесты с функциями

func TestTransformUser(t *testing.T) {
tests := []struct {
name string
input *User
want *User
wantErr error
}{
{
name: "valid user",
input: &User{Name: "john doe", Email: "JOHN@EXAMPLE.COM"},
want: &User{Name: "John Doe", Email: "john@example.com"},
},
{
name: "empty name",
input: &User{Name: "", Email: "john@example.com"},
wantErr: ErrEmptyName,
},
{
name: "invalid email",
input: &User{Name: "John", Email: "invalid"},
wantErr: ErrInvalidEmail,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := TransformUser(tt.input)

if !errors.Is(err, tt.wantErr) {
t.Errorf("TransformUser() error = %v, want %v", err, tt.wantErr)
return
}

if tt.wantErr == nil {
if got.Name != tt.want.Name {
t.Errorf("Name = %q, want %q", got.Name, tt.want.Name)
}
if got.Email != tt.want.Email {
t.Errorf("Email = %q, want %q", got.Email, tt.want.Email)
}
}
})
}
}

3. Табличные тесты с моками

func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID int64
mockSetup func(*mockUserRepo)
wantUser *User
wantErr error
}{
{
name: "user found",
userID: 1,
mockSetup: func(m *mockUserRepo) {
m.On("FindByID", mock.Anything, int64(1)).
Return(&User{ID: 1, Name: "John"}, nil)
},
wantUser: &User{ID: 1, Name: "John"},
},
{
name: "user not found",
userID: 999,
mockSetup: func(m *mockUserRepo) {
m.On("FindByID", mock.Anything, int64(999)).
Return(nil, ErrNotFound)
},
wantErr: ErrNotFound,
},
{
name: "database error",
userID: 1,
mockSetup: func(m *mockUserRepo) {
m.On("FindByID", mock.Anything, int64(1)).
Return(nil, errors.New("connection refused"))
},
wantErr: ErrInternal,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := new(mockUserRepo)
tt.mockSetup(repo)

svc := NewUserService(repo)
user, err := svc.GetUser(context.Background(), tt.userID)

if !errors.Is(err, tt.wantErr) {
t.Errorf("GetUser() error = %v, want %v", err, tt.wantErr)
return
}

if tt.wantUser != nil {
assert.Equal(t, tt.wantUser, user)
}

repo.AssertExpectations(t)
})
}
}

4. Профилирование с pprof

import _ "net/http/pprof"

func main() {
// Запуск pprof сервера
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// Основная логика
runApplication()
}

5. Использование pprof

# CPU профиль (30 секунд)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Профиль кучи
go tool pprof http://localhost:6060/debug/pprof/heap

# Профиль горутин
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Профиль блокировок
go tool pprof http://localhost:6060/debug/pprof/block

# Визуализация в браузере
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile

6. Программное профилирование

func profileCPU() {
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()

if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}
defer pprof.StopCPUProfile()

// Код для профилирования
expensiveOperation()
}

func profileMemory() {
f, err := os.Create("mem.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()

// Код для профилирования
allocateMemory()

if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal(err)
}
}

7. Анализ результатов pprof

# Топ функций по CPU
go tool pprof -top cpu.prof

# Топ по выделению памяти
go tool pprof -top -alloc_space mem.prof

# Генерация графа вызовов
go tool pprof -web cpu.prof

# Интерактивный режим
go tool pprof cpu.prof
(pprof) top
(pprof) list functionName
(pprof) web

8. Бенчмарки с профилированием

func BenchmarkProcessRequest(b *testing.B) {
// Профилирование во время бенчмарка
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
b.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
processRequest()
}
}

9. Трассировка выполнения

func traceExecution() {
f, err := os.Create("trace.out")
if err != nil {
log.Fatal(err)
}
defer f.Close()

if err := trace.Start(f); err != nil {
log.Fatal(err)
}
defer trace.Stop()

// Код для трассировки
concurrentOperation()
}

# Анализ трассировки
go tool trace trace.out

10. Рекомендации

  • Используйте табличные тесты для параметризованных тестов
  • Используйте t.Run() для именования подтестов
  • Запускайте тесты с -race для обнаружения гонок
  • Используйте pprof для анализа производительности
  • Профилируйте в production с осторожностью
  • Сравнивайте бенчмарки через benchstat

Итог: Табличные тесты — стандарт в Go для параметризованных тестов. pprof — мощный инструмент для анализа CPU, памяти, горутин. Используйте оба инструмента для повышения качества и производительности кода.

Вопрос 32. Чем отличаются процессы и потоки в Linux?

Таймкод: 01:45:19

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

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

Понимание различий между процессами и потоками важно для разработки эффективных конкурентных приложений. В Linux это особенно актуально из-за особенностей реализации.

1. Процесс

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

func processExample() {
// Создание нового процесса в Go
cmd := exec.Command("ls", "-la")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
}

2. Поток (Thread)

// Поток — это единица выполнения внутри процесса
// Разделяет адресное пространство с другими потоками процесса
// Имеет собственный стек и регистры
// В Linux реализован через clone()

func threadExample() {
// В Go горутины — это не потоки ОС
// Но для понимания потоков:

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Это горутина, не поток ОС
// Но концепция похожа
fmt.Printf("Goroutine %d\n", id)
}(i)
}
wg.Wait()
}

3. Сравнение процессов и потоков

ХарактеристикаПроцессПоток
Адресное пространствоСобственноеОбщее с процессом
ПамятьИзолированнаяРазделяемая
СозданиеДорогое (~мс)Дешевле (~мкс)
Переключение контекстаДорогоеДешевле
ИзоляцияПолнаяНет
КоммуникацияIPC (каналы, сокеты)Общая память
ПадениеНе влияет на другиеУбивает весь процесс

4. Модель памяти процесса

Процесс:
┌─────────────────────────────┐
│ Стек (Stack) │ ← Локальные переменные, вызовы функций
├─────────────────────────────┤
│ ↓ │
│ (рост вниз) │
│ │
│ (рост вверх) │
│ ↑ │
├─────────────────────────────┤
│ Куча (Heap) │ ← Динамическое выделение памяти
├─────────────────────────────┤
│ Данные (Data/BSS) │ ← Глобальные переменные
├─────────────────────────────┤
│ Код (Text) │ ← Инструкции программы
└─────────────────────────────┘

5. Модель памяти потоков

Процесс с потоками:
┌─────────────────────────────────────────────┐
│ Общая память процесса │
│ ┌─────────────────────────────────────────┐ │
│ │ Куча (Heap) │ │
│ ├─────────────────────────────────────────┤ │
│ │ Данные (Data/BSS) │ │
│ ├─────────────────────────────────────────┤ │
│ │ Код (Text) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Поток 1: │
│ ┌─────────────────────────────────────────┐ │
│ │ Стек потока 1 │ │
│ │ Регистры потока 1 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Поток 2: │
│ ┌─────────────────────────────────────────┐ │
│ │ Стек потока 2 │ │
│ │ Регистры потока 2 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

6. Создание процессов в Linux

// В C (для понимания)
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork(); // Создание нового процесса

if (pid == 0) {
// Дочерний процесс
// Имеет копию адресного пространства родителя
execlp("ls", "ls", "-la", NULL);
} else if (pid > 0) {
// Родительский процесс
wait(NULL); // Ожидание завершения дочернего
}

return 0;
}

7. Создание потоков в Linux

// В C (для понимания)
#include <pthread.h>

void* thread_func(void* arg) {
// Поток выполняет эту функцию
// Имеет собственный стек
// Разделяет кучу и данные с другими потоками
int* value = (int*)arg;
printf("Thread: %d\n", *value);
return NULL;
}

int main() {
pthread_t thread;
int value = 42;

// Создание потока
pthread_create(&thread, NULL, thread_func, &value);

// Ожидание завершения потока
pthread_join(thread, NULL);

return 0;
}

8. Горутины vs потоки ОС

func goroutineVsThread() {
// Горутины в Go:
// - Легковесные (~2-8 КБ стека)
// - Управляются рантаймом Go
// - Могут быть сотни тысяч
// - Переключаются в user-space

// Потоки ОС:
// - Тяжёлые (~1-8 МБ стека)
// - Управляются ядром
// - Обычно тысячи
// - Переключаются через ядро

// Модель Go: M горутин на N потоков ОС
runtime.GOMAXPROCS(4) // 4 потока ОС

var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Легковесная горутина
}()
}
wg.Wait()
}

9. Коммуникация между процессами

func processCommunication() {
// 1. Каналы (pipes)
cmd1 := exec.Command("echo", "hello world")
cmd2 := exec.Command("wc", "-w")

pipe, _ := cmd1.StdoutPipe()
cmd2.Stdin = pipe

cmd2.Start()
cmd1.Start()
cmd1.Wait()
cmd2.Wait()

// 2. Сокеты
// 3. Разделяемая память (mmap)
// 4. Сигналы
// 5. Очереди сообщений
}

10. Коммуникация между потоками/горутинами

func goroutineCommunication() {
// 1. Каналы (предпочтительный способ)
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch

// 2. Общая память с синхронизацией
var mu sync.Mutex
var shared int

go func() {
mu.Lock()
shared = 42
mu.Unlock()
}()

mu.Lock()
fmt.Println(shared)
mu.Unlock()

// 3. sync.WaitGroup
// 4. sync.Cond
// 5. atomic
}

11. Рекомендации

  • Используйте горутины вместо потоков ОС в Go
  • Используйте каналы для коммуникации между горутинами
  • Используйте мьютексы для защиты общего состояния
  • Избегайте разделяемой памяти без синхронизации
  • Используйте sync/atomic для простых операций

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

Вопрос 33. Какие сигналы используются для завершения процессов в Linux и чем они отличаются?

Таймкод: 01:47:24

Ответ собеседника: Правильный. SIGKILL (-9) — немедленное завершение процесса, сигнал нельзя перехватить или проигнорировать — это как «вырвать провод из розетки». SIGTERM (-15) — запрос на завершение, который можно перехватить и обработать для graceful shutdown. Также есть SIGINT (Ctrl+C) и другие сигналы для межпроцессной коммуникации.

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

Сигналы — это механизм межпроцессной коммуникации в Linux. Понимание различных сигналов важно для корректного управления жизненным циклом приложений.

1. Основные сигналы завершения

func signalOverview() {
// SIGTERM (15) — запрос на завершение (graceful shutdown)
// SIGKILL (9) — немедленное завершение (нельзя перехватить)
// SIGINT (2) — прерывание (Ctrl+C)
// SIGQUIT (3) — выход с дампом ядра (Ctrl+\)
// SIGHUP (1) — закрытие терминала
}

2. Обработка сигналов в Go

func signalHandling() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

go func() {
sig := <-sigChan
fmt.Printf("Received signal: %v\n", sig)
// Graceful shutdown
cleanup()
os.Exit(0)
}()

// Основная работа
select {}
}

3. Graceful shutdown с SIGTERM

func gracefulShutdown() {
srv := &http.Server{Addr: ":8080"}

// Обработка сигналов
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// Запуск сервера
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

// Ожидание сигнала
sig := <-sigChan
fmt.Printf("Received signal: %v\n", sig)

// Graceful shutdown с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
log.Printf("Shutdown error: %v", err)
}

fmt.Println("Server stopped gracefully")
}

4. Сравнение сигналов

СигналНомерПерехватОписание
SIGTERM15ДаЗапрос на завершение
SIGKILL9НетНемедленное завершение
SIGINT2ДаCtrl+C
SIGQUIT3ДаCtrl+\
SIGHUP1ДаЗакрытие терминала

5. Отправка сигналов

# SIGTERM (по умолчанию)
kill <pid>

# SIGKILL
kill -9 <pid>
kill -SIGKILL <pid>

# SIGINT
kill -2 <pid>
kill -SIGINT <pid>

# Список всех сигналов
kill -l

6. Обработка нескольких сигналов

func multipleSignals() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGHUP,
syscall.SIGUSR1,
syscall.SIGUSR2,
)

for sig := range sigChan {
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
fmt.Println("Shutdown signal received")
cleanup()
return
case syscall.SIGHUP:
fmt.Println("Reload config signal received")
reloadConfig()
case syscall.SIGUSR1:
fmt.Println("User signal 1 received")
handleUserSignal1()
case syscall.SIGUSR2:
fmt.Println("User signal 2 received")
handleUserSignal2()
}
}
}

7. Игнорирование сигналов

func ignoreSignals() {
// Игнорирование сигнала
signal.Ignore(syscall.SIGHUP)

// Сброс обработчика к поведению по умолчанию
signal.Reset(syscall.SIGHUP)
}

8. Паттерn: перезагрузка конфигурации

func configReload() {
config := loadConfig()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP)

go func() {
for range sigChan {
fmt.Println("Reloading config...")
newConfig, err := loadConfig()
if err != nil {
log.Printf("Failed to reload config: %v", err)
continue
}
config = newConfig
fmt.Println("Config reloaded")
}
}()
}

9. Паттерn: завершение с таймаутом

func shutdownWithTimeout() {
done := make(chan struct{})

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)

go func() {
<-sigChan

// Даём время на graceful shutdown
select {
case <-done:
fmt.Println("Graceful shutdown completed")
case <-time.After(30 * time.Second):
fmt.Println("Forced shutdown after timeout")
}

os.Exit(0)
}()

// Основная работа
doWork()
close(done)
}

10. Рекомендации

  • Всегда обрабатывайте SIGTERM для graceful shutdown
  • Используйте SIGKILL только как крайнюю меру
  • Используйте SIGHUP для перезагрузки конфигурации
  • Используйте SIGUSR1/SIGUSR2 для пользовательских команд
  • Устанавливайте таймаут для graceful shutdown

Итог: SIGTERM — для graceful shutdown (можно перехватить), SIGKILL — для немедленного завершения (нельзя перехватить). Обрабатывайте сигналы для корректного завершения приложений.

Вопрос 34. Какие способы межпроцессной коммуникации (IPC) существуют в Linux?

Таймкод: 01:48:21

Ответ собеседника: Правильный. Основные способы IPC: 1) Разделяемая память (shared memory) — быстрый, но сложный в управлении, требует синхронизации. 2) Сокеты (sockets) — Unix domain sockets работают через файловую систему. 3) Pipes (каналы) — односторонняя передача данных, блокирует писателя пока читатель не прочитает. 4) Очереди сообщений (message queues). Go рекомендует использовать каналы вместо low-level IPC.

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

IPC (Inter-Process Communication) — это механизмы для обмена данными между процессами. Linux предоставляет множество способов IPC, каждый из которых имеет свои преимущества.

1. Обзор механизмов IPC

Механизмы IPC в Linux:
┌─────────────────────────────────────────────┐
│ 1. Pipes (каналы) │
│ 2. Named Pipes (FIFO) │
│ 3. Unix Domain Sockets │
│ 4. Network Sockets │
│ 5. Shared Memory (разделяемая память) │
│ 6. Message Queues (очереди сообщений) │
│ 7. Signals (сигналы) │
│ 8. Memory-Mapped Files (mmap) │
│ 9. D-Bus │
└─────────────────────────────────────────────┘

2. Pipes (каналы)

func anonymousPipe() {
// Анонимный канал — односторонняя связь между родителем и дочерним процессом

cmd := exec.Command("wc", "-w")

// Получаем pipe для stdin дочернего процесса
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}

// Запускаем команду
if err := cmd.Start(); err != nil {
log.Fatal(err)
}

// Пишем в pipe
go func() {
defer stdin.Close()
io.WriteString(stdin, "hello world from Go")
}()

// Читаем результат
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}

fmt.Printf("Words: %s", output)
}

func namedPipe() {
// Named pipe (FIFO) — файл в файловой системе
fifoPath := "/tmp/myfifo"

// Создание FIFO (через syscall)
syscall.Mkfifo(fifoPath, 0666)

// Запись в FIFO
go func() {
f, _ := os.OpenFile(fifoPath, os.O_WRONLY, 0666)
defer f.Close()
f.WriteString("Hello from FIFO\n")
}()

// Чтение из FIFO
f, _ := os.OpenFile(fifoPath, os.O_RDONLY, 0666)
defer f.Close()

buf := make([]byte, 1024)
n, _ := f.Read(buf)
fmt.Printf("Received: %s", buf[:n])
}

3. Unix Domain Sockets

func unixDomainSocket() {
socketPath := "/tmp/test.sock"

// Сервер
go func() {
os.Remove(socketPath)

listener, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatal(err)
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
continue
}

go func(c net.Conn) {
defer c.Close()
buf := make([]byte, 1024)
n, _ := c.Read(buf)
fmt.Printf("Server received: %s\n", buf[:n])
c.Write([]byte("Hello from server"))
}(conn)
}
}()

time.Sleep(100 * time.Millisecond)

// Клиент
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

conn.Write([]byte("Hello from client"))

buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Printf("Client received: %s\n", buf[:n])
}

4. Shared Memory (разделяемая память)

func sharedMemoryExample() {
// Создаём разделяемую память через mmap
// Используем файл как бэкенд

f, err := os.CreateTemp("", "shm")
if err != nil {
log.Fatal(err)
}
defer os.Remove(f.Name())

// Устанавливаем размер файла
f.Truncate(4096)

// Mmap файл
data, err := syscall.Mmap(
int(f.Fd()), 0, 4096,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED,
)
if err != nil {
log.Fatal(err)
}
defer syscall.Munmap(data)

// Запись в разделяемую память
copy(data, []byte("Hello from shared memory!"))

// Чтение из разделяемой памяти
fmt.Printf("Data: %s\n", string(data[:256]))
}

5. Сравнение механизмов IPC

МеханизмСкоростьСложностьНаправлениеОбласть
PipesСредняяПростаяОдностороннееРодственники
Named PipesСредняяПростаяОдностороннееЛюбые процессы
Unix SocketsВысокаяСредняяДвустороннееОдна машина
Network SocketsСредняяСредняяДвустороннееСеть
Shared MemoryОчень высокаяСложнаяДвустороннееОдна машина
Message QueuesСредняяСредняяДвустороннееЛюбые процессы
SignalsВысокаяПростаяОдностороннееЛюбые процессы
mmapОчень высокаяСложнаяДвустороннееЛюбые процессы

6. Каналы Go vs IPC

func goChannels() {
// Каналы Go — высокоуровневый IPC
ch := make(chan string, 10)

// Отправитель
go func() {
for i := 0; i < 5; i++ {
ch <- fmt.Sprintf("Message %d", i)
}
close(ch)
}()

// Получатель
for msg := range ch {
fmt.Println("Received:", msg)
}
}

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

func whenToUseWhat() {
// Pipes:
// - Простая односторонняя связь между процессами
// - Связь родитель-дочерний процесс

// Named Pipes:
- Связь между неродственными процессами на одной машине

// Unix Domain Sockets:
// - Двусторонняя связь на одной машине
// - Высокая производительность

// Shared Memory:
// - Максимальная производительность
// - Большие объёмы данных
// - Требует синхронизации

// Message Queues:
// - Асинхронная коммуникация
// - Гарантированная доставка

// Go Channels:
// - Коммуникация между горутинами
// - Самый простой способ в Go
}

8. Паттерn: клиент-сервер через Unix Socket

type Server struct {
socketPath string
listener net.Listener
}

func NewServer(socketPath string) *Server {
return &Server{socketPath: socketPath}
}

func (s *Server) Start(handler func(net.Conn)) error {
os.Remove(s.socketPath)

listener, err := net.Listen("unix", s.socketPath)
if err != nil {
return err
}
s.listener = listener

for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handler(conn)
}
}

func (s *Server) Stop() {
if s.listener != nil {
s.listener.Close()
}
os.Remove(s.socketPath)
}

9. Рекомендации

  • Используйте каналы Go для коммуникации между горутинами
  • Используйте Unix Domain Sockets для IPC на одной машине
  • Используйте pipes для простой односторонней связи
  • Используйте shared memory для максимальной производительности
  • Избегайте сигналов для передачи данных (только для управления)

Итог: Linux предоставляет множество механизмов IPC: pipes, sockets, shared memory, message queues, signals. Выбор зависит от требований к производительности, сложности и типу коммуникации. В Go предпочтительнее использовать каналы для горутин и Unix sockets для межпроцессной коммуникации.

Вопрос 35. Чем отличаются TCP и UDP и где они применяются?

Таймкод: 01:51:14

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

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

TCP и UDP — два основных транспортных протокола. Понимание их различий критически важно для выбора правильного протокола в зависимости от задачи.

1. Сравнение TCP и UDP

ХарактеристикаTCPUDP
СоединениеС установлением соединенияБез соединения
Гарантия доставкиДаНет
Порядок доставкиГарантированНе гарантирован
СкоростьМедленнееБыстрее
Нагрузка на сетьВышеНиже
Размер заголовка20-60 байт8 байт
Управление потокомДаНет
Управление перегрузкойДаНет

2. TCP — подробнее

func tcpServer() {
// TCP сервер в Go
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
continue
}

go handleTCPConnection(conn)
}
}

func handleTCPConnection(conn net.Conn) {
defer conn.Close()

// TCP гарантирует:
// 1. Доставку данных
// 2. Порядок доставки
// 3. Целостность данных

buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
return
}

data := buf[:n]
fmt.Printf("Received: %s\n", data)

// Ответ
conn.Write([]byte("ACK"))
}
}

func tcpClient() {
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer conn.Close()

// Отправка данных
conn.Write([]byte("Hello TCP"))

// Получение ответа
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
fmt.Printf("Response: %s\n", buf[:n])
}

3. UDP — подробнее

func udpServer() {
// UDP сервер в Go
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Fatal(err)
}

conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

buf := make([]byte, 4096)
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}

data := buf[:n]
fmt.Printf("Received from %s: %s\n", remoteAddr, data)

// Ответ (не гарантирован)
conn.WriteToUDP([]byte("ACK"), remoteAddr)
}
}

func udpClient() {
addr, err := net.ResolveUDPAddr("udp", "localhost:8080")
if err != nil {
log.Fatal(err)
}

conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

// Отправка датаграммы
conn.Write([]byte("Hello UDP"))

// Получение ответа (может не прийти)
buf := make([]byte, 4096)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, _ := conn.Read(buf)
fmt.Printf("Response: %s\n", buf[:n])
}

4. Трёхстороннее рукопожатие TCP

Клиент Сервер
| |
|-------- SYN ----------->| (1) Клиент запрашивает соединение
| |
|<---- SYN+ACK -----------| (2) Сервер подтверждает
| |
|-------- ACK ----------->| (3) Клиент подтверждает
| |
|===== Соединение установлено =====|
| |
|-------- DATA ---------->| (4) Передача данных
| |
|<-------- ACK -----------| (5) Подтверждение получения

5. Гарантии TCP

func tcpGuarantees() {
// TCP гарантирует:

// 1. Доставку данных
// - Если пакет потерян, он отправляется повторно
// - Используются подтверждения (ACK)

// 2. Порядок доставки
// - Пакеты нумеруются
// - Получатель собирает в правильном порядке

// 3. Целостность данных
// - Контрольные суммы
// - Повторная отправка при ошибках

// 4. Управление потоком
// - Окно получателя
// - Предотвращение переполнения

// 5. Управление перегрузкой
// - Slow start
// - Congestion avoidance
}

6. Применение TCP

func tcpUseCases() {
// TCP используется когда важна надёжность:

// HTTP/HTTPS — веб-сайты
// SMTP — электронная почта
// FTP — передача файлов
// SSH — удалённый доступ
// PostgreSQL/MySQL — базы данных
// gRPC — RPC-вызовы
// WebSocket — реальное время с гарантиями

// Пример: HTTP сервер
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello HTTP over TCP"))
})
http.ListenAndServe(":8080", nil)
}

7. Применение UDP

func udpUseCases() {
// UDP используется когда важна скорость:

// DNS — быстрые запросы
// DHCP — получение IP
// VoIP — голосовые звонки
// Видеостриминг — YouTube, Twitch
// Онлайн игры — быстрый отклик
// IoT — датчики, телеметрия
// QUIC — новый протокол (HTTP/3)

// Пример: DNS запрос
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}

ips, _ := resolver.LookupHost(context.Background(), "google.com")
fmt.Println(ips)
}

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

func performanceComparison() {
// TCP:
// - Задержка: выше (рукопожатие, подтверждения)
// - Пропускная способность: ниже (overhead)
// - Надёжность: высокая

// UDP:
// - Задержка: ниже (нет рукопожатия)
// - Пропускная способность: выше (меньше overhead)
// - Надёжность: низкая

// Пример: отправка 1000 сообщений
// TCP: ~50ms (с подтверждениями)
// UDP: ~10ms (без подтверждений)
}

9. Реализация надёжности поверх UDP

type ReliableUDP struct {
conn *net.UDPConn
seqNum uint32
ackChan map[uint32]chan bool
mu sync.Mutex
}

func (r *ReliableUDP) SendWithAck(data []byte, addr *net.UDPAddr) error {
seq := atomic.AddUint32(&r.seqNum, 1)

// Добавляем номер последовательности
packet := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(packet[:4], seq)
copy(packet[4:], data)

// Отправляем
_, err := r.conn.WriteToUDP(packet, addr)
if err != nil {
return err
}

// Ждём подтверждения
ackChan := make(chan bool, 1)
r.mu.Lock()
r.ackChan[seq] = ackChan
r.mu.Unlock()

select {
case <-ackChan:
return nil
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
}

10. Рекомендации

  • Используйте TCP для данных, где важна надёжность
  • Используйте UDP для данных, где важна скорость
  • Реализуйте надёжность поверх UDP если нужно
  • Используйте QUIC для лучшего из двух миров
  • Тестируйте под нагрузкой для выбора оптимального протокола

Итог: TCP — надёжный, но медленнее. UDP — быстрый, но без гарантий. Выбор зависит от требований: надёжность vs скорость. В Go оба протокола легко используются через пакет net.

Вопрос 35. Чем отличаются TCP и UDP и где они применяются?

Таймкод: 01:51:14

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

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

TCP и UDP — два основных транспортных протокола. Понимание их различий критически важно для выбора правильного протокола в зависимости от задачи.

1. Сравнение TCP и UDP

ХарактеристикаTCPUDP
СоединениеС установлением соединенияБез соединения
Гарантия доставкиДаНет
Порядок доставкиГарантированНе гарантирован
СкоростьМедленнееБыстрее
Нагрузка на сетьВышеНиже
Размер заголовка20-60 байт8 байт
Управление потокомДаНет
Управление перегрузкойДаНет

2. TCP — подробнее

func tcpServer() {
// TCP сервер в Go
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
continue
}

go handleTCPConnection(conn)
}
}

func handleTCPConnection(conn net.Conn) {
defer conn.Close()

// TCP гарантирует:
// 1. Доставку данных
// 2. Порядок доставки
// 3. Целостность данных

buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
return
}

data := buf[:n]
fmt.Printf("Received: %s\n", data)

// Ответ
conn.Write([]byte("ACK"))
}
}

func tcpClient() {
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer conn.Close()

// Отправка данных
conn.Write([]byte("Hello TCP"))

// Получение ответа
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
fmt.Printf("Response: %s\n", buf[:n])
}

3. UDP — подробнее

func udpServer() {
// UDP сервер в Go
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Fatal(err)
}

conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

buf := make([]byte, 4096)
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}

data := buf[:n]
fmt.Printf("Received from %s: %s\n", remoteAddr, data)

// Ответ (не гарантирован)
conn.WriteToUDP([]byte("ACK"), remoteAddr)
}
}

func udpClient() {
addr, err := net.ResolveUDPAddr("udp", "localhost:8080")
if err != nil {
log.Fatal(err)
}

conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

// Отправка датаграммы
conn.Write([]byte("Hello UDP"))

// Получение ответа (может не прийти)
buf := make([]byte, 4096)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, _ := conn.Read(buf)
fmt.Printf("Response: %s\n", buf[:n])
}

4. Трёхстороннее рукопожатие TCP

Клиент Сервер
| |
|-------- SYN ----------->| (1) Клиент запрашивает соединение
| |
|<---- SYN+ACK -----------| (2) Сервер подтверждает
| |
|-------- ACK ----------->| (3) Клиент подтверждает
| |
|===== Соединение установлено =====|
| |
|-------- DATA ---------->| (4) Передача данных
| |
|<-------- ACK -----------| (5) Подтверждение получения

5. Гарантии TCP

func tcpGuarantees() {
// TCP гарантирует:

// 1. Доставку данных
// - Если пакет потерян, он отправляется повторно
// - Используются подтверждения (ACK)

// 2. Порядок доставки
// - Пакеты нумеруются
// - Получатель собирает в правильном порядке

// 3. Целостность данных
// - Контрольные суммы
// - Повторная отправка при ошибках

// 4. Управление потоком
// - Окно получателя
// - Предотвращение переполнения

// 5. Управление перегрузкой
// - Slow start
// - Congestion avoidance
}

6. Применение TCP

func tcpUseCases() {
// TCP используется когда важна надёжность:

// HTTP/HTTPS — веб-сайты
// SMTP — электронная почта
// FTP — передача файлов
// SSH — удалённый доступ
// PostgreSQL/MySQL — базы данных
// gRPC — RPC-вызовы
// WebSocket — реальное время с гарантиями

// Пример: HTTP сервер
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello HTTP over TCP"))
})
http.ListenAndServe(":8080", nil)
}

7. Применение UDP

func udpUseCases() {
// UDP используется когда важна скорость:

// DNS — быстрые запросы
// DHCP — получение IP
// VoIP — голосовые звонки
// Видеостриминг — YouTube, Twitch
// Онлайн игры — быстрый отклик
// IoT — датчики, телеметрия
// QUIC — новый протокол (HTTP/3)

// Пример: DNS запрос
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}

ips, _ := resolver.LookupHost(context.Background(), "google.com")
fmt.Println(ips)
}

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

func performanceComparison() {
// TCP:
// - Задержка: выше (рукопожатие, подтверждения)
// - Пропускная способность: ниже (overhead)
// - Надёжность: высокая

// UDP:
// - Задержка: ниже (нет рукопожатия)
// - Пропускная способность: выше (меньше overhead)
// - Надёжность: низкая

// Пример: отправка 1000 сообщений
// TCP: ~50ms (с подтверждениями)
// UDP: ~10ms (без подтверждений)
}

9. Реализация надёжности поверх UDP

type ReliableUDP struct {
conn *net.UDPConn
seqNum uint32
ackChan map[uint32]chan bool
mu sync.Mutex
}

func (r *ReliableUDP) SendWithAck(data []byte, addr *net.UDPAddr) error {
seq := atomic.AddUint32(&r.seqNum, 1)

// Добавляем номер последовательности
packet := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(packet[:4], seq)
copy(packet[4:], data)

// Отправляем
_, err := r.conn.WriteToUDP(packet, addr)
if err != nil {
return err
}

// Ждём подтверждения
ackChan := make(chan bool, 1)
r.mu.Lock()
r.ackChan[seq] = ackChan
r.mu.Unlock()

select {
case <-ackChan:
return nil
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
}

10. Рекомендации

  • Используйте TCP для данных, где важна надёжность
  • Используйте UDP для данных, где важна скорость
  • Реализуйте надёжность поверх UDP если нужно
  • Используйте QUIC для лучшего из двух миров
  • Тестируйте под нагрузкой для выбора оптимального протокола

Итог: TCP — надёжный, но медленнее. UDP — быстрый, но без гарантий. Выбор зависит от требований: надёжность vs скорость. В Go оба протокола легко используются через пакет net.

Вопрос 36. Чем отличаются SQL и NoSQL базы данных?

Таймкод: 01:56:53

Ответ собеседника: Правильный. SQL базы — реляционные, используют структурированные таблицы со схемой, поддерживают ACID транзакции и сложные запросы с JOIN. Хранят данные в форме, близкой к бизнес-сущностям. NoSQL — документоориентированные, ключ-значение и т.д. Оптимизированы для быстрого доступа, горизонтального масштабирования и гибкой схемы. Позволяют динамически группировать данные и записывать данные без строгой схемы. SQL подходит для структурированных данных со сложными связями, NoSQL — для быстро меняющихся требований и стартапов.

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

SQL и NoSQL — два подхода к хранению данных. Понимание их различий помогает выбрать правильную базу данных для конкретной задачи.

1. Основные различия

ХарактеристикаSQLNoSQL
Модель данныхРеляционная (таблицы)Документная, ключ-значение, графовая, колоночная
СхемаСтрогая, предопределённаяГибкая, динамическая
МасштабированиеВертикальноеГоризонтальное
ТранзакцииACIDBASE (Eventually Consistent)
ЗапросыSQLСпецифичные API
СвязиJOIN, внешние ключиДенормализация, вложенные документы

2. SQL базы данных

// Пример: PostgreSQL в Go
func sqlExample() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()

// Создание таблицы
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
log.Fatal(err)
}

// Вставка данных
_, err = db.Exec(
"INSERT INTO users (name, email) VALUES ($1, $2)",
"John Doe", "john@example.com",
)
if err != nil {
log.Fatal(err)
}

// Запрос с JOIN
rows, err := db.Query(`
SELECT u.name, u.email, o.order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.email = $1
`, "john@example.com")
if err != nil {
log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
var name, email string
var orderID int
var total float64
rows.Scan(&name, &email, &orderID, &total)
fmt.Printf("User: %s, Order: %d, Total: %.2f\n", name, orderID, total)
}
}

3. NoSQL базы данные

// Пример: MongoDB в Go
func nosqlExample() {
ctx := context.Background()

client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(ctx)

collection := client.Database("mydb").Collection("users")

// Вставка документа (гибкая схема)
doc := bson.M{
"name": "John Doe",
"email": "john@example.com",
"address": bson.M{
"street": "123 Main St",
"city": "New York",
},
"tags": []string{"golang", "mongodb"},
"created_at": time.Now(),
}

_, err = collection.InsertOne(ctx, doc)
if err != nil {
log.Fatal(err)
}

// Поиск
var result bson.M
err = collection.FindOne(ctx, bson.M{"email": "john@example.com"}).Decode(&result)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found: %v\n", result)
}

4. ACID vs BASE

func acidVsBase() {
// ACID (SQL):
// A - Atomicity (Атомарность)
// C - Consistency (Согласованность)
// I - Isolation (Изолированность)
// D - Durability (Долговечность)

// Пример ACID транзакции в PostgreSQL
/*
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
*/

// BASE (NoSQL):
// B - Basically Available (Базовая доступность)
// S - Soft state (Мягкое состояние)
// E - Eventually consistent (Конечная согласованность)

// В NoSQL данные могут быть временно несогласованы,
// но в конечном итоге станут согласованными
}

5. Типы NoSQL баз данных

func nosqlTypes() {
// 1. Документные (MongoDB, CouchDB)
// - Хранят документы (JSON/BSON)
// - Гибкая схема
// - Пример: каталог товаров

// 2. Ключ-значение (Redis, Memcached)
// - Простая модель: ключ -> значение
// - Очень быстрые операции
// - Пример: кэш, сессии

// 3. Колоночные (Cassandra, HBase)
// - Хранят данные по столбцам
// - Хорошо для аналитики
// - Пример: логи, временные ряды

// 4. Графовые (Neo4j, Amazon Neptune)
// - Узлы и связи
// - Пример: социальные сети, рекомендации
}

6. Когда использовать SQL

func whenToUseSQL() {
// SQL подходит когда:

// 1. Данные структурированы и имеют чёткую схему
// - Пользователи, заказы, платежи

// 2. Нужны сложные запросы с JOIN
// - Отчёты, аналитика

// 3. Важна согласованность данных
// - Финансовые операции, банковские системы

// 4. Нужны ACID транзакции
// - Переводы между счетами

// Пример SQL запроса
query := `
SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5
ORDER BY total_spent DESC
`
fmt.Println(query)
}

7. Когда использовать NoSQL

func whenToUseNoSQL() {
// NoSQL подходит когда:

// 1. Схема данных часто меняется
// - Стартапы, прототипы

// 2. Нужно горизонтальное масштабирование
// - Большие объёмы данных

// 3. Важна скорость записи/чтения
// - Кэш, сессии, логи

// 4. Данные не требуют сложных JOIN
// - Документы, профили пользователей

// Пример: Redis для кэша
/*
SET user:12345:session "{\"user_id\": 12345, \"role\": \"admin\"}"
GET user:12345:session
*/
}

8. Сравнение производительности

func performanceComparison() {
// SQL:
// - Медленнее при больших объёмах
// - Быстрее для сложных запросов
// - Ограничено вертикальным масштабированием

// NoSQL:
// - Быстрее при простых операциях
// - Хуже для сложных запросов
// - Легко масштабируется горизонтально

// Пример запроса в MongoDB
query := `
db.users.aggregate([
{ $match: { status: "active" } },
{ $group: { _id: "$city", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])
`
fmt.Println(query)
}

9. Гибридный подход

func hybridApproach() {
// Часто используют оба подхода:

// SQL для:
// - Пользователей
// - Заказов
// - Платежей

// NoSQL для:
// - Кэша (Redis)
// - Логов (Elasticsearch)
// - Сессий (Redis)
// - Аналитики (ClickHouse)

// Пример архитектуры:
// PostgreSQL (основные данные)
// Redis (кэш)
// Elasticsearch (поиск)
// ClickHouse (аналитика)
}

10. Рекомендации

  • Используйте SQL для структурированных данных со сложными связями
  • Используйте NoSQL для гибкой схемы и горизонтального масштабирования
  • Рассмотрите гибридный подход для сложных систем
  • Начинайте с SQL, добавляйте NoSQL по мере необходимости
  • Выбирайте базу данных на основе требований, не моды

Итог: SQL — для структурированных данных с ACID транзакциями. NoSQL — для гибкой схемы и горизонтального масштабирования. Выбор зависит от требований к данным, согласованности и масштабированию. Часто используют оба подхода в одной системе.

Вопрос 37. Что такое индексы в базах данных и какие типы индексов существуют?

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

Ответ собеседника: Правильный. Индекс — это структура данных (обычно B-tree), которая хранит карту доступа к элементам, позволяя быстрее находить данные по определённым полям. Типы: 1) B-tree — стандартный, для точного поиска и диапазонов. 2) GiST — для геометрических данных, полигонов, координат (GIS). 3) GIN — для полнотекстового поиска. Индексы нужно чистить (VACUUM), иначе они разрастаются и замедляют вставку. Нельзя создать индексы на все сочетания полей — это дорого по памяти и требует перестройки при изменениях.

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

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

1. Что такое индекс

Без индекса: Seq Scan (последовательный перебор)
┌─────────────────────────────────────┐
│ Строка 1: id=5, name="Alice" │ ← проверяем
│ Строка 2: id=3, name="Bob" │ ← проверяем
│ Строка 3: id=8, name="Charlie" │ ← проверяем
│ ... │
│ Строка N: id=1, name="Dave" │ ← нашли!
└─────────────────────────────────────┘
O(N) — линейный поиск

С индексом: Index Scan (поиск по индексу)
┌─────────────────────────────────────┐
│ B-tree индекс по id: │
│ [5] │
│ / \ │
│ [3] [8] │
│ / \ / \ │
│ [1] [2] [6] [9] │ ← нашли за O(log N)
└─────────────────────────────────────┘
O(log N) — логарифмический поиск

2. B-tree индекс (стандартный)

-- Создание B-tree индекса
CREATE INDEX idx_users_email ON users (email);

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

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

-- Частичный индекс
CREATE INDEX idx_users_active ON users (email) WHERE active = true;
// Пример использования B-tree индекса в Go
func queryWithIndex(db *sql.DB) {
// Этот запрос использует индекс idx_users_email
rows, err := db.Query("SELECT * FROM users WHERE email = $1", "john@example.com")
if err != nil {
log.Fatal(err)
}
defer rows.Close()

// Этот запрос использует индекс idx_users_name_email
rows2, err := db.Query("SELECT * FROM users WHERE name = $1 AND email = $2", "John", "john@example.com")
if err != nil {
log.Fatal(err)
}
defer rows2.Close()
}

3. GIN индекс (Generalized Inverted Index)

-- GIN индекс для массивов и JSONB
CREATE INDEX idx_users_tags ON users USING GIN (tags);

-- GIN индекс для полнотекстового поиска
CREATE INDEX idx_users_search ON users USING GIN (to_tsvector('english', name || ' ' || bio));

-- GIN индекс для JSONB
CREATE INDEX idx_users_data ON users USING GIN (data);
// Пример использования GIN индекса
func queryWithGINIndex(db *sql.DB) {
// Поиск по массиву тегов
rows, err := db.Query("SELECT * FROM users WHERE tags @> ARRAY[$1]", "golang")
if err != nil {
log.Fatal(err)
}
defer rows.Close()

// Поиск по JSONB
rows2, err := db.Query("SELECT * FROM users WHERE data @> $1", `{"city": "New York"}`)
if err != nil {
log.Fatal(err)
}
defer rows2.Close()
}

4. GiST индекс (Generalized Search Tree)

-- GiST индекс для геометрических данных
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);

-- GiST индекс для диапазонов
CREATE INDEX idx_events_period ON events USING GIST (period);

-- GiST индекс для полнотекстового поиска
CREATE INDEX idx_documents_content ON documents USING GIST (to_tsvector('english', content));
// Пример использования GiST индекса
func queryWithGiSTIndex(db *sql.DB) {
// Поиск ближайших точек
rows, err := db.Query(`
SELECT * FROM locations
ORDER BY geom <-> ST_MakePoint($1, $2)
LIMIT 10
`, -73.9857, 40.7484) // Координаты Нью-Йорка
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}

5. BRIN индекс (Block Range Index)

-- BRIN индекс для больших таблиц с упорядоченными данными
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at);

-- BRIN индекс с параметрами
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at) WITH (pages_per_range = 32);
// Пример использования BRIN индекса
func queryWithBRINIndex(db *sql.DB) {
// BRIN эффективен для больших таблиц с упорядоченными данными
rows, err := db.Query(`
SELECT * FROM logs
WHERE created_at >= $1 AND created_at < $2
`, "2024-01-01", "2024-01-02")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}

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

ТипРазмерСкорость поискаСкорость вставкиКогда использовать
B-treeСреднийO(log N)СредняяТочный поиск, диапазоны
GINБольшойO(1) для массивовМедленнаяМассивы, JSONB, FTS
GiSTСреднийO(log N)СредняяГеометрия, диапазоны
BRINМаленькийO(N/k)БыстраяБольшие упорядоченные таблицы
HashМаленькийO(1)БыстраяТолько точный поиск

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

-- Проверить, используется ли индекс
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'john@example.com';

-- Результат:
-- Index Scan using idx_users_email on users
-- Index Cond: (email = 'john@example.com'::text)
-- Planning Time: 0.1 ms
-- Execution Time: 0.05 ms

-- Без индекса было бы:
-- Seq Scan on users
-- Filter: (email = 'john@example.com'::text)
-- Rows Removed by Filter: 999999
-- Planning Time: 0.1 ms
-- Execution Time: 500 ms
// Функция для анализа запроса
func analyzeQuery(db *sql.DB, query string, args ...interface{}) {
rows, err := db.Query("EXPLAIN ANALYZE "+query, args...)
if err != nil {
log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
var plan string
rows.Scan(&plan)
fmt.Println(plan)
}
}

8. Обслуживание индексов

-- Перестроить индекс
REINDEX INDEX idx_users_email;

-- Перестроить все индексы таблицы
REINDEX TABLE users;

-- Удалить неиспользуемый индекс
DROP INDEX idx_unused_index;

-- Найти неиспользуемые индексы
SELECT
indexrelname AS index_name,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
// Функция для мониторинга индексов
func monitorIndexes(db *sql.DB) {
rows, err := db.Query(`
SELECT
indexrelname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC
LIMIT 10
`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()

fmt.Println("Неиспользуемые индексы:")
for rows.Next() {
var name, size string
var scans int
rows.Scan(&name, &scans, &size)
fmt.Printf(" %s: %d scans, size: %s\n", name, scans, size)
}
}

9. Рекомендации по индексированию

func indexingRecommendations() {
// 1. Индексируйте поля в WHERE, JOIN, ORDER BY
// 2. Используйте составные индексы для частых комбинаций
// 3. Не создавайте слишком много индексов
// 4. Мониторьте использование индексов
// 5. Удаляйте неиспользуемые индексы

// Пример оптимального индексирования
queries := []string{
// Для запроса: WHERE email = ?
"CREATE INDEX idx_users_email ON users (email)",

// Для запроса: WHERE name = ? AND email = ?
"CREATE INDEX idx_users_name_email ON users (name, email)",

// Для запроса: WHERE created_at > ? ORDER BY created_at
"CREATE INDEX idx_users_created_at ON users (created_at)",

// Для запроса: WHERE active = true AND email = ?
"CREATE INDEX idx_users_active_email ON users (email) WHERE active = true",
}

for _, q := range queries {
fmt.Println(q)
}
}

10. Антипаттерны

-- ПЛОХО: Индекс на каждое поле
CREATE INDEX idx1 ON users (name);
CREATE INDEX idx2 ON users (email);
CREATE INDEX idx3 ON users (created_at);
CREATE INDEX idx4 ON users (name, email);
CREATE INDEX idx5 ON users (name, created_at);
CREATE INDEX idx6 ON users (email, created_at);
-- Слишком много индексов!

-- ХОРОШО: Несколько составных индексов
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_name_email ON users (name, email);
CREATE INDEX idx_users_created_at ON users (created_at);

-- ПЛОХО: Индекс на поле с низкой кардинальностью
CREATE INDEX idx_users_gender ON users (gender); -- только 2-3 значения

-- ХОРОШО: Частичный индекс
CREATE INDEX idx_users_active ON users (email) WHERE active = true;

Итог: Индексы ускоряют поиск за счёт дополнительного места и замедления записи. B-tree — стандартный тип. GIN — для массивов и JSONB. GiST — для геометрии. BRIN — для больших упорядоченных таблиц. Важно мониторить использование индексов и удалять неиспользуемые.

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

Таймкод: 02:04:22

Ответ собеседника: Правильный. Serializable — максимальный уровень изоляции, который гарантирует, что параллельные транзакции дают тот же результат, что и последовательное выполнение. Достигается через Snapshot Isolation (SSI): каждая транзакция работает со своим снимком данных. В конце проверяется, не было ли конфликтов — если два процесса изменяли одни и те же данные, одна транзакция откатывается и повторяется. Это не последовательное выполнение (медленно), а параллельное с проверкой конфликтов. Для масштабирования используется вертикальное масштабирование (больше ресурсов на одной машине), а не горизонтальное реплицирование.

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

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

1. Уровни изоляции

┌─────────────────────────────────────────────────────────────────┐
│ Уровни изоляции │
├──────────────────┬──────────────┬──────────────┬───────────────┤
│ Уровень │ Dirty Read │ Non-Repeatable│ Phantom Read │
│ │ │ Read │ │
├──────────────────┼──────────────┼──────────────┼───────────────┤
│ Read Uncommitted │ Возможен │ Возможен │ Возможен │
│ Read Committed │ Невозможен │ Возможен │ Возможен │
│ Repeatable Read │ Невозможен │ Невозможен │ Возможен │
│ Serializable │ Невозможен │ Невозможен │ Невозможен │
└──────────────────┴──────────────┴──────────────┴───────────────┘

2. Проблемы, которые решает Serializable

func isolationProblems() {
// 1. Dirty Read (Грязное чтение)
// Транзакция A читает незафиксированные данные транзакции B
// Если B откатит изменения, A прочитает несуществующие данные

// 2. Non-Repeatable Read (Неповторяющееся чтение)
// Транзакция A читает данные дважды
// Между чтениями транзакция B изменяет и фиксирует эти данные
// A получает разные результаты

// 3. Phantom Read (Фантомное чтение)
// Транзакция A читает набор строк дважды
// Между чтениями транзакция B добавляет новые строки
// A получает разное количество строк

// 4. Serialization Anomaly (Аномалия сериализации)
// Параллельные транзакции дают результат,
// который невозможен при любом последовательном порядке
}

3. Пример аномалии сериализации

-- Транзакция A: Перевод денег со счёта 1 на счёт 2
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1; -- 100
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

-- Транзакция B: Перевод денег со счёта 2 на счёт 1
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 2; -- 100
UPDATE accounts SET balance = balance - 30 WHERE id = 2;
UPDATE accounts SET balance = balance + 30 WHERE id = 1;
COMMIT;

-- Без Serializable:
-- A читает balance=100 на счёте 1
-- B читает balance=100 на счёте 2
-- A вычитает 50 со счёта 1
-- B вычитает 30 со счёта 2
-- Оба фиксируют изменения
-- Итог: счёт 1 = 80, счёт 2 = 120
-- Но при последовательном выполнении было бы: счёт 1 = 80, счёт 2 = 120
-- Это нормально, но бывают случаи, когда результат невозможен

4. Реализация Serializable в PostgreSQL (SSI)

// PostgreSQL использует Serializable Snapshot Isolation (SSI)
// Это не блокировки, а отслеживание зависимостей

func ssiExample(db *sql.DB) {
tx, err := db.BeginTx(context.Background(), &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()

// Транзакция работает со своим снимком данных
var balance int
err = tx.QueryRow("SELECT balance FROM accounts WHERE id = $1", 1).Scan(&balance)
if err != nil {
log.Fatal(err)
}

// Изменение данных
_, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2", 50, 1)
if err != nil {
log.Fatal(err)
}

// При фиксации PostgreSQL проверяет конфликты
// Если конфликт обнаружен — ошибка serialization_failure
err = tx.Commit()
if err != nil {
// Ошибка: could not serialize access due to read/write dependencies
// Нужно повторить транзакку
log.Printf("Serialization conflict, retrying: %v", err)
}
}

5. Обработка конфликтов

func executeWithRetry(db *sql.DB, maxRetries int, fn func(*sql.Tx) error) error {
for i := 0; i < maxRetries; i++ {
tx, err := db.BeginTx(context.Background(), &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return err
}

err = fn(tx)
if err != nil {
tx.Rollback()
return err
}

err = tx.Commit()
if err != nil {
// Проверяем, что это ошибка сериализации
if isSerializationError(err) {
tx.Rollback()
// Экспоненциальная задержка перед повтором
time.Sleep(time.Duration(i*i) * 10 * time.Millisecond)
continue
}
tx.Rollback()
return err
}

return nil // Успех
}
return fmt.Errorf("max retries exceeded")
}

func isSerializationError(err error) bool {
// PostgreSQL error code 40001 = serialization_failure
return strings.Contains(err.Error(), "40001") ||
strings.Contains(err.Error(), "could not serialize")
}

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

func isolationLevels() {
levels := []struct {
Name string
GoLevel sql.IsolationLevel
Description string
}{
{
Name: "Read Uncommitted",
GoLevel: sql.LevelReadUncommitted,
Description: "Можно читать незафиксированные данные",
},
{
Name: "Read Committed",
GoLevel: sql.LevelReadCommitted,
Description: "Только зафиксированные данные (по умолчанию в PostgreSQL)",
},
{
Name: "Repeatable Read",
GoLevel: sql.LevelRepeatableRead,
Description: "Повторное чтение возвращает те же данные",
},
{
Name: "Serializable",
GoLevel: sql.LevelSerializable,
Description: "Полная изоляция, эквивалент последовательного выполнения",
},
}

for _, level := range levels {
fmt.Printf("%s: %s\n", level.Name, level.Description)
}
}

7. Когда использовать Serializable

func whenToUseSerializable() {
// Используйте Serializable когда:

// 1. Финансовые операции
// Переводы между счетами, платежи

// 2. Инвентарь
// Резервирование товаров, бронирование

// 3. Системы с жёсткими ограничениями
// Балансы не могут быть отрицательными

// 4. Когда важна консистентность выше производительности

// НЕ используйте Serializable когда:
// 1. Высокая нагрузка на запись
// 2. Много конфликтов между транзакциями
// 3. Производительность важнее консистентности
}

8. Пример: Бронирование билетов

func bookTicket(db *sql.DB, userID, eventID int) error {
return executeWithRetry(db, 5, func(tx *sql.Tx) error {
// Проверяем доступность билетов
var available int
err := tx.QueryRow(`
SELECT available_tickets FROM events WHERE id = $1
`, eventID).Scan(&available)
if err != nil {
return err
}

if available <= 0 {
return fmt.Errorf("no tickets available")
}

// Бронируем билет
_, err = tx.Exec(`
UPDATE events SET available_tickets = available_tickets - 1 WHERE id = $1
`, eventID)
if err != nil {
return err
}

// Создаём запись о бронировании
_, err = tx.Exec(`
INSERT INTO bookings (user_id, event_id, created_at)
VALUES ($1, $2, NOW())
`, userID, eventID)
if err != nil {
return err
}

return nil
})
}

9. Производительность Serializable

func serializablePerformance() {
// Serializable медленнее других уровней изоляции

// Причины:
// 1. Отслеживание зависимостей между транзакциями
// 2. Проверка конфликтов при фиксации
// 3. Необходимость повторения транзакций при конфликтах

// Рекомендации:
// 1. Делайте транзакции короткими
// 2. Минимизируйте количество операций в транзакции
// 3. Используйте только когда необходимо
// 4. Рассмотрите оптимистические блокировки как альтернативу
}

10. Альтернативы Serializable

func alternativesToSerializable() {
// 1. Оптимистические блокировки
// Версионирование записей

// 2. Пессимистические блокировки
// SELECT ... FOR UPDATE

// 3. Уровень Repeatable Read
// Достаточно для большинства случаев

// 4. Прикладная логика
// Проверка ограничений в коде
}

Итог: Serializable — максимальный уровень изоляции, гарантирующий эквивалентность последовательному выполнению. Реализуется через SSI (отслеживание зависимостей + проверка конфликтов). Требует повторения транзакций при конфликтах. Используйте только когда консистентность критична, иначе предпочитайте более низкие уровни изоляции.

Вопрос 39. Чем отличаются Data Race и Race Condition?

Таймкод: 02:10:58

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

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

Data Race и Race Condition — связанные, но различные понятия. Data Race — это конкретный случай Race Condition, связанный с параллельным доступом к памяти.

1. Определения

┌─────────────────────────────────────────────────────────────────┐
│ Race Condition (Широкое понятие) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Data Race (Частный случай) │ │
│ │ │ │
│ │ Два потока одновременно обращаются к одной ячейке │ │
│ │ памяти без синхронизации, и хотя бы один пишет │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Другие Race Condition: │
│ - Check-Then-Act │
│ - Read-Modify-Write │
│ - Порядок операций │
└─────────────────────────────────────────────────────────────────┘

2. Data Race (гонка данных)

// Пример Data Race
func dataRaceExample() {
var counter int

// Горутина 1
go func() {
for i := 0; i < 1000; i++ {
counter++ // Data Race: чтение + запись без синхронизации
}
}()

// Горутина 2
go func() {
for i := 0; i < 1000; i++ {
counter++ // Data Race: чтение + запись без синхронизации
}
}()

time.Sleep(time.Second)
fmt.Println(counter) // Результат непредсказуем!
}
// Решение: использование мьютекса
func noDataRaceExample() {
var counter int
var mu sync.Mutex

// Горутина 1
go func() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}()

// Горутина 2
go func() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}()

time.Sleep(time.Second)
fmt.Println(counter) // 2000
}

3. Race Condition (гонка условий)

// Пример Race Condition без Data Race
func raceConditionExample() {
var balance int64 = 100

// Горутина 1: проверка и списание
go func() {
// Check
if balance >= 50 {
// Другой поток может изменить balance здесь!
time.Sleep(time.Millisecond) // Имитация задержки
// Act
balance -= 50 // Race Condition: баланс мог измениться
}
}()

// Горутина 2: проверка и списание
go func() {
if balance >= 50 {
time.Sleep(time.Millisecond)
balance -= 50 // Race Condition: баланс мог стать отрицательным
}
}()

time.Sleep(time.Second)
fmt.Println(balance) // Может быть 0 или -50!
}

4. Типы Race Condition

func typesOfRaceConditions() {
// 1. Check-Then-Act (Проверка, затем действие)
// Проверяем условие, потом действуем
// Между проверкой и действием состояние могло измениться

// 2. Read-Modify-Write (Чтение-Изменение-Запись)
// Читаем значение, модифицируем, записываем
// Другой поток мог изменить значение между чтением и записью

// 3. Порядок операций
// Результат зависит от порядка выполнения операций
// Порядок может быть разным при каждом запуске
}

5. Пример Check-Then-Act

func checkThenActExample() {
var cache = make(map[string]string)
var mu sync.RWMutex

// Горутина 1
go func() {
// Check
mu.RLock()
_, exists := cache["key"]
mu.RUnlock()

if !exists {
// Другой поток мог добавить "key" здесь!
time.Sleep(time.Millisecond)

// Act
mu.Lock()
cache["key"] = "value1" // Может перезаписать значение другого потока
mu.Unlock()
}
}()

// Горутина 2
go func() {
mu.RLock()
_, exists := cache["key"]
mu.RUnlock()

if !exists {
time.Sleep(time.Millisecond)
mu.Lock()
cache["key"] = "value2" // Может перезаписать значение другого потока
mu.Unlock()
}
}()
}
// Решение: атомарная операция
func checkThenActSolution() {
var cache = make(map[string]string)
var mu sync.Mutex

getValue := func(key string) (string, bool) {
mu.Lock()
defer mu.Unlock()

val, exists := cache[key]
return val, exists
}

setValue := func(key, value string) {
mu.Lock()
defer mu.Unlock()

if _, exists := cache[key]; !exists {
cache[key] = value
}
}

_ = getValue
_ = setValue
}

6. Пример Read-Modify-Write

func readModifyWriteExample() {
var counter int64

// Горутина 1
go func() {
// Read
value := atomic.LoadInt64(&counter)

// Modify
newValue := value + 1

// Другой поток мог изменить counter здесь!
time.Sleep(time.Millisecond)

// Write
atomic.StoreInt64(&counter, newValue) // Потеря инкремента!
}()

// Горутина 2
go func() {
value := atomic.LoadInt64(&counter)
newValue := value + 1
time.Sleep(time.Millisecond)
atomic.StoreInt64(&counter, newValue) // Потеря инкремента!
}()
}
// Решение: атомарная операция
func readModifyWriteSolution() {
var counter int64

// Горутина 1
go func() {
atomic.AddInt64(&counter, 1) // Атомарная операция
}()

// Горутина 2
go func() {
atomic.AddInt64(&counter, 1) // Атомарная операция
}()
}

7. Обнаружение Data Race

# Использование Go Race Detector
go run -race main.go
go test -race ./...
go build -race -o app
// Пример, который найдёт Race Detector
func raceDetectorExample() {
var data int

go func() {
data++ // WARNING: DATA RACE
}()

go func() {
data++ // WARNING: DATA RACE
}()

time.Sleep(time.Second)
}

8. Сравнение

ХарактеристикаData RaceRace Condition
ОпределениеОдновременный доступ к памяти без синхронизацииЗависимость результата от порядка выполнения
ОбнаружениеGo Race Detector, ThreadSanitizerТолько анализ кода
РешениеМьютексы, атомарные операцииПравильная синхронизация, дизайн
ОтношениеЧастный случайОбщее понятие

9. Практические рекомендации

func bestPractices() {
// 1. Используйте -race при тестировании
// go test -race ./...

// 2. Минимизируйте общие данные
// Лучше передавать данные через каналы

// 3. Используйте иммутабельные данные
// Если данные не меняются — нет Race Condition

// 4. Предпочитайте каналы мьютексам
// "Don't communicate by sharing memory; share memory by communicating"

// 5. Используйте sync/atomic для простых операций
// atomic.AddInt64, atomic.LoadInt64 и т.д.

// 6. Держите критические секции короткими
// Минимизируйте время удержания блокировки
}

10. Пример с каналами (без Race Condition)

func channelExample() {
// Используем каналы вместо общей памяти
counter := make(chan int, 1)
counter <- 0 // Начальное значение

done := make(chan bool)

// Горутина 1
go func() {
for i := 0; i < 1000; i++ {
val := <-counter
val++
counter <- val
}
done <- true
}()

// Горутина 2
go func() {
for i := 0; i < 1000; i++ {
val := <-counter
val++
counter <- val
}
done <- true
}()

// Ждём завершения
<-done
<-done

final := <-counter
fmt.Println(final) // 2000 — всегда правильно
}

Итог: Data Race — это конкретный случай Race Condition, когда несколько потоков одновременно обращаются к одной ячейке памяти без синхронизации. Race Condition — более широкое понятие, включающее любые ситуации, где результат зависит от порядка выполнения. Data Race можно обнаружить автоматически (Go Race Detector), Race Condition — только анализом кода.

Вопрос 40. Чем отличаются брокеры сообщений Kafka и RabbitMQ?

Таймкод: 02:14:11

Ответ собеседника: Правильный. Kafka — распределённый лог сообщений, который гарантирует сохранность данных на диске. Медленнее, но безопаснее — можно упасть и перечитать все заново. RabbitMQ — быстрее, но менее надёжный в плане сохранности. Kafka сохраняет все сообщения, что позволяет перечитывать историю. В Kafka есть партиции — максимальная параллельность чтения ограничена количеством партиций. Если потребителей больше, чем партиций, лишние будут простаивать.

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

Kafka и RabbitMQ — принципиально разные системы, заточенные под разные сценарии. Kafka — распределённый лог с персистентностью, RabbitMQ — классический брокер сообщений.

1. Архитектурные различия

┌─────────────────────────────────────────────────────────────────┐
│ Kafka │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Broker 1 │ │Broker 2 │ │Broker 3 │ (Кластер) │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────▼────────────▼────────────▼────┐ │
│ │ Topic (Лог) │ │
│ │ ┌──────────┬──────────┬─────────┐│ │
│ │ │Partition │Partition │Partition││ │
│ │ │ 0 │ 1 │ 2 ││ │
│ │ └──────────┴──────────┴─────────┘│ │
│ └───────────────────────────────────┘ │
│ │
│ Сообщения хранятся на диске, можно перечитывать историю │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ RabbitMQ │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ Broker │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Exchange │ │ │
│ │ └───────────┬─────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼─────────────┐ │ │
│ │ │ Queue │ │ │
│ │ └───────────┬─────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼─────────────┐ │ │
│ │ │ Consumer │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
│ │
│ Сообщения удаляются после обработки (по умолчанию) │
└─────────────────────────────────────────────────────────────────┘

2. Модель доставки

// Kafka: Pull-модель (потребитель сам забирает сообщения)
func kafkaConsumer() {
// Потребитель сам решает, когда читать
// Можно перечитать старые сообщения
// Можно упасть и продолжить с того же места

// Гарантия доставки:
// - At-least-once (по умолчанию)
// - Exactly-once (с транзакциями)
}

// RabbitMQ: Push-модель (брокер отправляет сообщения)
func rabbitMQConsumer() {
// Брокер сам отправляет сообщения
// Сообщение удаляется из очереди после ack
// Нельзя перечитать старые сообщения

// Гарантия доставки:
// - At-most-once (auto-ack)
// - At-least-once (manual ack)
}

3. Персистентность сообщений

// Kafka: сообщения хранятся на диске
func kafkaPersistence() {
// Сообщения сохраняются в лог на диске
// Можно настроить время хранения (retention)
// Можно перечитать историю с любого момента

// Настройки:
// - retention.ms: время хранения (по умолчанию 7 дней)
// - retention.bytes: максимальный размер лога
// - cleanup.policy: delete или compact
}

// RabbitMQ: сообщения в памяти (по умолчанию)
func rabbitMQPersistence() {
// Сообщения хранятся в памяти
// Можно включить персистентность (durable queue)
// Но это замедляет работу

// Настройки:
// - durable: очередь сохраняется после перезапуска
// - delivery_mode=2: сообщение сохраняется на диск
// - TTL: время жизни сообщения
}

4. Партиционирование и параллелизм

// Kafka: партиции
func kafkaPartitions() {
// Топик разделён на партиции
// Каждая партиция — упорядоченный лог
// Параллелизм = количество партиций

// Пример:
// Topic "orders" с 3 партициями
// - Партиция 0: [msg1, msg4, msg7, ...]
// - Партиция 1: [msg2, msg5, msg8, ...]
// - Партиция 2: [msg3, msg6, msg9, ...]

// Максимум 3 потребителя одновременно
// Если потребителей больше — лишние простаивают
}

// RabbitMQ: очереди
func rabbitMQQueues() {
// Одна очередь может обслуживать многих потребителей
// Сообщения распределяются round-robin
// Нет ограничения на количество потребителей

// Пример:
// Queue "orders"
// - Consumer 1: msg1, msg4, msg7, ...
// - Consumer 2: msg2, msg5, msg8, ...
// - Consumer 3: msg3, msg6, msg9, ...
}

5. Маршрутизация сообщений

// Kafka: простая маршрутизация
func kafkaRouting() {
// Сообщения отправляются в топик
// Ключ определяет партицию
// Без ключа — round-robin по партициям

// Пример:
// producer.Send("orders", key="user123", value=order)
// Все сообщения user123 попадут в одну партицию
// Это гарантирует порядок для одного пользователя
}

// RabbitMQ: гибкая маршрутизация
func rabbitMQRouting() {
// Exchange маршрутизирует сообщения в очереди
// Типы exchange:
// - Direct: точное совпадение routing key
// - Topic: паттерны (orders.*, orders.created.#)
// - Fanout: во все привязанные очереди
// - Headers: по заголовкам

// Пример:
// exchange.Publish("orders.created", order)
// → Queue "orders.created"
// → Queue "orders.all"
// → Queue "audit"
}

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

// Kafka: высокая пропускная способность
func kafkaPerformance() {
// Оптимизирована для записи
// Пакетная отправка (batching)
// Сжатие сообщений
// Zero-copy чтение

// Характеристики:
// - Пропускная способность: ~100K-1M msg/sec
// - Латентность: 5-50ms
// - Размер сообщений: до 1MB (по умолчанию)
}

// RabbitMQ: низкая латентность
func rabbitMQPerformance() {
// Оптимизирована для низкой латентности
// Мгновенная доставка
// Меньше накладных расходов

// Характеристики:
// - Пропускная способность: ~10K-50K msg/sec
// - Латентность: 0.5-5ms
// - Размер сообщений: до 128MB
}

7. Гарантии порядка

// Kafka: порядок внутри партиции
func kafkaOrdering() {
// Сообщения упорядочены внутри партиции
// Нет глобального порядка между партициями

// Пример:
// Партиция 0: [msg1, msg2, msg3] — порядок гарантирован
// Партиция 1: [msg4, msg5, msg6] — порядок гарантирован
// Но msg1 и msg4 могут быть обработаны в любом порядке
}

// RabbitMQ: порядок в очереди
func rabbitMQOrdering() {
// Сообщения упорядочены в очереди (FIFO)
// Но при нескольких потребителях порядок не гарантирован

// Пример:
// Queue: [msg1, msg2, msg3, msg4]
// Consumer 1: msg1, msg3
// Consumer 2: msg2, msg4
// Порядок обработки: msg1, msg2, msg3, msg4 (не гарантирован)
}

8. Сравнительная таблица

ХарактеристикаKafkaRabbitMQ
МодельРаспределённый логБрокер сообщений
ХранениеДиск (персистентно)Память (по умолчанию)
Перечитать историюДаНет
Модель доставкиPullPush
ПараллелизмПо партициямНе ограничен
МаршрутизацияПростая (топик + ключ)Гибкая (exchange)
Пропускная способностьВысокаяСредняя
ЛатентностьСредняяНизкая
СложностьВысокаяСредняя
ЭкосистемаKafka Connect, KSQLПлагины, интеграции

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

// Kafka подходит для:
func kafkaUseCases() {
// 1. Потоковая обработка данных
// - Логи, метрики, события

// 2. Event Sourcing
// - Хранение всех изменений состояния

// 3. Интеграция микросервисов
// - Асинхронная коммуникация

// 4. Обработка больших объёмов данных
// - Высокая пропускная способность

// 5. Когда нужна персистентность
// - Возможность перечитать историю
}

// RabbitMQ подходит для:
func rabbitMQUseCases() {
// 1. Очереди задач
// - Фоновая обработка

// 2. RPC (Remote Procedure Call)
// - Синхронные запросы

// 3. Маршрутизация сообщений
// - Сложная логика маршрутизации

// 4. Низкая латентность
// - Мгновенная доставка

// 5. Простые сценарии
// - Меньше сложности
}

10. Примеры кода

// Kafka Producer (с использованием sarama)
func kafkaProducer() {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll
config.Producer.Retry.Max = 5
config.Producer.Return.Successes = true

producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
if err != nil {
log.Fatal(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
Topic: "orders",
Key: sarama.StringEncoder("user123"),
Value: sarama.StringEncoder(`{"order_id": 1, "amount": 100}`),
}

partition, offset, err := producer.SendMessage(msg)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Message sent to partition %d at offset %d\n", partition, offset)
}

// RabbitMQ Producer (с использованием amqp)
func rabbitMQProducer() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatal(err)
}
defer conn.Close()

ch, err := conn.Channel()
if err != nil {
log.Fatal(err)
}
defer ch.Close()

q, err := ch.QueueDeclare(
"orders", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatal(err)
}

body := `{"order_id": 1, "amount": 100}`
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: []byte(body),
},
)
if err != nil {
log.Fatal(err)
}
}

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

Вопрос 41. Как спроектировать Notification Center с гарантией exactly-once доставки при наличии нескольких дата-центров?

Таймкод: 02:17:23

Ответ собеседника: Правильный. Архитектура: 1) Сервисы S1, S2, S3 отправляют сообщения в Kafka с указанием типа уведомления (SMS, push, email). 2) Notification Center читает из Kafka и сохраняет в хранилище (Redis/PostgreSQL). 3) Для дедупликации используется уникальный ID сообщения (композиция ID сервиса + тип сообщения + время или хэш). 4) Worker вычитывает новые записи и отправляет через нужные каналы. 5) Для гарантии exactly-once при нескольких дата-центров используется Leader Election — только один инстанс отправляет сообщения. 6) База данных реплицируется между дата-центрами (master-slave или multimaster). 7) Для обработки зависших сообщений используется Worker Controller с тайм-аутами. 8) Уведомление об отправке возвращается исходному сервису через отдельный топик Kafka (паттерн Saga).

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

Проектирование Notification Center с exactly-once доставкой и поддержкой нескольких дата-центров — сложная архитектурная задача, требующая решения проблем дедупликации, консистентности и отказоустойчивости.

1. Общая архитектура

┌─────────────────────────────────────────────────────────────────────────────┐
│ Notification Center │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Service 1│ │Service 2│ │Service 3│ (Источники событий) │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Kafka │ (Единая шина событий) │
│ │ Cluster │ │
│ └───────┬───────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌──▼───┐ ┌───▼───┐ ┌────▼────┐ │
│ │ DC 1 │ │ DC 2 │ │ DC 3 │ (Дата-центры) │
│ │ │ │ │ │ │ │
│ │┌────┐│ │┌─────┐│ │┌───────┐│ │
│ ││Lead││ ││Stand││ ││Standby││ (Leader Election) │
│ ││ er ││ ││ by ││ ││ ││ │
│ │└────┘│ │└─────┘│ │└───────┘│ │
│ └──────┘ └───────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

2. Генерация уникального ID сообщения

package notification

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)

// MessageID генерирует уникальный идентификатор сообщения
// для обеспечения идемпотентности
type MessageID struct {
ServiceID string // ID сервиса-источника
MessageType string // Тип уведомления (sms, push, email)
RecipientID string // ID получателя
EventID string // ID события
Timestamp time.Time // Время создания
}

func (m *MessageID) Generate() string {
// Композиция полей для уникальности
raw := fmt.Sprintf("%s:%s:%s:%s:%d",
m.ServiceID,
m.MessageType,
m.RecipientID,
m.EventID,
m.Timestamp.UnixNano(),
)

hash := sha256.Sum256([]byte(raw))
return hex.EncodeToString(hash[:16])
}

// Пример использования
func exampleMessageID() {
msgID := MessageID{
ServiceID: "order-service",
MessageType: "email",
RecipientID: "user-12345",
EventID: "order-created-67890",
Timestamp: time.Now(),
}

id := msgID.Generate()
// id: "a1b2c3d4e5f6g7h8" — уникальный ID для дедупликации
}

3. Структура сообщения

package notification

import "time"

// NotificationEvent — событие уведомления
type NotificationEvent struct {
ID string `json:"id"` // Уникальный ID (для дедупликации)
ServiceID string `json:"service_id"` // Сервис-источник
MessageType string `json:"message_type"` // sms, push, email
Recipient Recipient `json:"recipient"` // Получатель
Content Content `json:"content"` // Содержимое
Priority Priority `json:"priority"` // Приоритет
Metadata map[string]string `json:"metadata"` // Дополнительные данные
CreatedAt time.Time `json:"created_at"` // Время создания
}

type Recipient struct {
ID string `json:"id"` // ID получателя
Email string `json:"email"` // Email
Phone string `json:"phone"` // Телефон
Token string `json:"token"` // Push token
}

type Content struct {
Subject string `json:"subject"` // Тема (для email)
Body string `json:"body"` // Текст
Template string `json:"template"` // ID шаблона
}

type Priority int

const (
PriorityLow Priority = iota
PriorityNormal
PriorityHigh
PriorityCritical
)

4. Producer — отправка событий в Kafka

package notification

import (
"context"
"encoding/json"
"log"
"time"

"github.com/segmentio/kafka-go"
)

// EventProducer отправляет события уведомлений в Kafka
type EventProducer struct {
writer *kafka.Writer
}

func NewEventProducer(brokers []string) *EventProducer {
return &EventProducer{
writer: &kafka.Writer{
Addr: kafka.TCP(brokers...),
Topic: "notifications",
Balancer: &kafka.Hash{},
BatchTimeout: 10 * time.Millisecond,
RequiredAcks: kafka.RequireAll, // Ждём подтверждения от всех реплик
Async: false, // Синхронная отправка для надёжности
},
}
}

// SendNotification отправляет уведомление с гарантией идемпотентности
func (p *EventProducer) SendNotification(ctx context.Context, event *NotificationEvent) error {
// Генерируем уникальный ID для дедупликации
msgID := MessageID{
ServiceID: event.ServiceID,
MessageType: event.MessageType,
RecipientID: event.Recipient.ID,
EventID: event.Metadata["event_id"],
Timestamp: event.CreatedAt,
}
event.ID = msgID.Generate()

// Сериализуем событие
value, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshal event: %w", err)
}

// Отправляем в Kafka с ID как ключом (для партиционирования)
err = p.writer.WriteMessages(ctx, kafka.Message{
Key: []byte(event.ID), // Ключ = ID для дедупликации
Value: value,
Headers: []kafka.Header{
{Key: "id", Value: []byte(event.ID)},
{Key: "service", Value: []byte(event.ServiceID)},
{Key: "type", Value: []byte(event.MessageType)},
},
})

if err != nil {
return fmt.Errorf("write message: %w", err)
}

log.Printf("Notification sent: id=%s, type=%s, recipient=%s",
event.ID, event.MessageType, event.Recipient.ID)

return nil
}

5. Consumer — чтение и сохранение в хранилище

package notification

import (
"context"
"encoding/json"
"fmt"
"log"
"time"

"github.com/segmentio/kafka-go"
)

// EventConsumer читает события из Kafka и сохраняет в БД
type EventConsumer struct {
reader *kafka.Reader
storage NotificationStorage
processor *NotificationProcessor
}

func NewEventConsumer(brokers []string, groupID string, storage NotificationStorage) *EventConsumer {
return &EventConsumer{
reader: kafka.NewReader(kafka.ReaderConfig{
Brokers: brokers,
GroupID: groupID,
Topic: "notifications",
MinBytes: 1e3, // 1KB
MaxBytes: 10e6, // 10MB
MaxWait: 500 * time.Millisecond,
ReadLagInterval: -1,
}),
storage: storage,
}
}

// Start запускает обработку событий
func (c *EventConsumer) Start(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

// Читаем сообщение из Kafka
msg, err := c.reader.ReadMessage(ctx)
if err != nil {
log.Printf("Read error: %v", err)
continue
}

// Десериализуем событие
var event NotificationEvent
if err := json.Unmarshal(msg.Value, &event); err != nil {
log.Printf("Unmarshal error: %v", err)
continue
}

// Сохраняем в БД с проверкой дубликата (idempotent write)
if err := c.storage.Save(ctx, &event); err != nil {
if IsDuplicateError(err) {
log.Printf("Duplicate notification skipped: %s", event.ID)
continue
}
log.Printf("Save error: %v", err)
continue
}

log.Printf("Notification saved: id=%s", event.ID)
}
}

6. Хранилище с дедупликацией

package notification

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

// NotificationStorage — интерфейс хранилища
type NotificationStorage interface {
Save(ctx context.Context, event *NotificationEvent) error
GetPending(ctx context.Context, limit int) ([]*NotificationEvent, error)
MarkSent(ctx context.Context, id string) error
MarkFailed(ctx context.Context, id string, reason string) error
}

// PostgresStorage — реализация на PostgreSQL
type PostgresStorage struct {
db *sql.DB
}

func NewPostgresStorage(db *sql.DB) *PostgresStorage {
return &PostgresStorage{db: db}
}

// Save сохраняет уведомление с проверкой дубликата
func (s *PostgresStorage) Save(ctx context.Context, event *NotificationEvent) error {
query := `
INSERT INTO notifications (id, service_id, message_type, recipient_id, content, status, created_at)
VALUES ($1, $2, $3, $4, $5, 'pending', $6)
ON CONFLICT (id) DO NOTHING
RETURNING id
`

content, _ := json.Marshal(event.Content)

var savedID string
err := s.db.QueryRowContext(ctx, query,
event.ID,
event.ServiceID,
event.MessageType,
event.Recipient.ID,
content,
event.CreatedAt,
).Scan(&savedID)

if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Дубликат — уже существует
return &DuplicateError{ID: event.ID}
}
return fmt.Errorf("insert notification: %w", err)
}

return nil
}

// GetPending получает неотправленные уведомления
func (s *PostgresStorage) GetPending(ctx context.Context, limit int) ([]*NotificationEvent, error) {
query := `
SELECT id, service_id, message_type, recipient_id, content, created_at
FROM notifications
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT $1
FOR UPDATE SKIP LOCKED
`

rows, err := s.db.QueryContext(ctx, query, limit)
if err != nil {
return nil, fmt.Errorf("query pending: %w", err)
}
defer rows.Close()

var events []*NotificationEvent
for rows.Next() {
var event NotificationEvent
var content []byte
err := rows.Scan(
&event.ID,
&event.ServiceID,
&event.MessageType,
&event.Recipient.ID,
&content,
&event.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("scan notification: %w", err)
}
json.Unmarshal(content, &event.Content)
events = append(events, &event)
}

return events, nil
}

// MarkSent помечает уведомление как отправленное
func (s *PostgresStorage) MarkSent(ctx context.Context, id string) error {
query := `
UPDATE notifications
SET status = 'sent', sent_at = $1
WHERE id = $2 AND status = 'pending'
`

result, err := s.db.ExecContext(ctx, query, time.Now(), id)
if err != nil {
return fmt.Errorf("mark sent: %w", err)
}

rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("notification not found or already processed: %s", id)
}

return nil
}

// MarkFailed помечает уведомление как неудачное
func (s *PostgresStorage) MarkFailed(ctx context.Context, id string, reason string) error {
query := `
UPDATE notifications
SET status = 'failed', error = $1, failed_at = $2
WHERE id = $3 AND status = 'pending'
`

_, err := s.db.ExecContext(ctx, query, reason, time.Now(), id)
return err
}

// DuplicateError — ошибка дубликата
type DuplicateError struct {
ID string
}

func (e *DuplicateError) Error() string {
return fmt.Sprintf("duplicate notification: %s", e.ID)
}

func IsDuplicateError(err error) bool {
var dupErr *DuplicateError
return errors.As(err, &dupErr)
}

7. Схема базы данных

-- Таблица уведомлений
CREATE TABLE notifications (
id VARCHAR(64) PRIMARY KEY, -- Уникальный ID (hash)
service_id VARCHAR(128) NOT NULL, -- Сервис-источник
message_type VARCHAR(32) NOT NULL, -- sms, push, email
recipient_id VARCHAR(128) NOT NULL, -- ID получателя
recipient_email VARCHAR(256),
recipient_phone VARCHAR(32),
recipient_token VARCHAR(512),
content JSONB NOT NULL, -- Содержимое
priority INTEGER DEFAULT 0, -- Приоритет
status VARCHAR(32) DEFAULT 'pending', -- pending, sent, failed
retry_count INTEGER DEFAULT 0, -- Количество попыток
max_retries INTEGER DEFAULT 3, -- Максимум попыток
error TEXT, -- Текст ошибки
created_at TIMESTAMP NOT NULL,
sent_at TIMESTAMP,
failed_at TIMESTAMP,

-- Индексы для быстрого поиска
INDEX idx_status_created (status, created_at),
INDEX idx_recipient (recipient_id),
INDEX idx_service (service_id)
);

-- Таблица для отслеживания обработанных сообщений (idempotency)
CREATE TABLE processed_messages (
message_id VARCHAR(64) PRIMARY KEY,
consumer_group VARCHAR(128) NOT NULL,
processed_at TIMESTAMP NOT NULL DEFAULT NOW(),

-- Автоматическая очистка старых записей
INDEX idx_processed_at (processed_at)
);

-- Таблица для Leader Election
CREATE TABLE leader_election (
service_name VARCHAR(128) PRIMARY KEY,
leader_id VARCHAR(128) NOT NULL, -- ID текущего лидера
dc_name VARCHAR(64) NOT NULL, -- Дата-центр
acquired_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL, -- Время истечения lease

INDEX idx_expires (expires_at)
);

8. Leader Election для мульти-DC

package notification

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

// LeaderElection реализует выбор лидера между дата-центрами
type LeaderElection struct {
db *sql.DB
serviceName string
instanceID string
dcName string
leaseDuration time.Duration
renewInterval time.Duration
}

func NewLeaderElection(db *sql.DB, serviceName, instanceID, dcName string) *LeaderElection {
return &LeaderElection{
db: db,
serviceName: serviceName,
instanceID: instanceID,
dcName: dcName,
leaseDuration: 30 * time.Second,
renewInterval: 10 * time.Second,
}
}

// Start запускает процесс выборов лидера
func (le *LeaderElection) Start(ctx context.Context) error {
// Пытаемся стать лидером
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

isLeader, err := le.tryAcquireLeadership(ctx)
if err != nil {
log.Printf("Leadership acquisition error: %v", err)
time.Sleep(le.renewInterval)
continue
}

if isLeader {
log.Printf("Became leader: %s (DC: %s)", le.instanceID, le.dcName)
return le.runAsLeader(ctx)
}

// Ждём следующей попытки
time.Sleep(le.renewInterval)
}
}

// tryAcquireLeadership пытается захватить лидерство
func (le *LeaderElection) tryAcquireLeadership(ctx context.Context) (bool, error) {
tx, err := le.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return false, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()

// Проверяем текущего лидера
var currentLeader string
var expiresAt time.Time

err = tx.QueryRowContext(ctx, `
SELECT leader_id, expires_at FROM leader_election WHERE service_name = $1
`, le.serviceName).Scan(&currentLeader, &expiresAt)

if err == sql.ErrNoRows {
// Нет лидера — создаём запись
_, err = tx.ExecContext(ctx, `
INSERT INTO leader_election (service_name, leader_id, dc_name, acquired_at, expires_at)
VALUES ($1, $2, $3, $4, $5)
`, le.serviceName, le.instanceID, le.dcName, time.Now(), time.Now().Add(le.leaseDuration))

if err != nil {
return false, fmt.Errorf("insert leader: %w", err)
}

tx.Commit()
return true, nil
}

if err != nil {
return false, fmt.Errorf("query leader: %w", err)
}

// Проверяем, не истёк ли lease
if time.Now().After(expiresAt) {
// Lease истёк — пытаемся захватить
result, err := tx.ExecContext(ctx, `
UPDATE leader_election
SET leader_id = $1, dc_name = $2, acquired_at = $3, expires_at = $4
WHERE service_name = $5 AND expires_at = $6
`, le.instanceID, le.dcName, time.Now(), time.Now().Add(le.leaseDuration),
le.serviceName, expiresAt)

if err != nil {
return false, fmt.Errorf("update leader: %w", err)
}

rows, _ := result.RowsAffected()
if rows > 0 {
tx.Commit()
return true, nil
}
}

// Уже есть активный лидер
return false, nil
}

// runAsLeader выполняет работу лидера
func (le *LeaderElection) runAsLeader(ctx context.Context) error {
renewTicker := time.NewTicker(le.renewInterval)
defer renewTicker.Stop()

for {
select {
case <-ctx.Done():
return ctx.Err()
case <-renewTicker.C:
// Продлеваем lease
if err := le.renewLeadership(ctx); err != nil {
log.Printf("Failed to renew leadership: %v", err)
return err
}
}
}
}

// renewLeadership продлевает lease лидера
func (le *LeaderElection) renewLeadership(ctx context.Context) error {
result, err := le.db.ExecContext(ctx, `
UPDATE leader_election
SET expires_at = $1
WHERE service_name = $2 AND leader_id = $3
`, time.Now().Add(le.leaseDuration), le.serviceName, le.instanceID)

if err != nil {
return fmt.Errorf("renew leadership: %w", err)
}

rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("lost leadership")
}

return nil
}

9. Worker — отправка уведомлений

package notification

import (
"context"
"log"
"time"
)

// NotificationWorker отправляет уведомления
type NotificationWorker struct {
storage NotificationStorage
processors map[string]MessageProcessor
leader *LeaderElection
}

type MessageProcessor interface {
Send(ctx context.Context, event *NotificationEvent) error
}

func NewWorker(storage NotificationStorage, leader *LeaderElection) *NotificationWorker {
return &NotificationWorker{
storage: storage,
processors: map[string]MessageProcessor{
"email": &EmailProcessor{},
"sms": &SMSProcessor{},
"push": &PushProcessor{},
},
leader: leader,
}
}

// Start запускает worker (только на лидере)
func (w *NotificationWorker) Start(ctx context.Context) error {
// Запускаем leader election
go func() {
if err := w.leader.Start(ctx); err != nil {
log.Printf("Leader election error: %v", err)
}
}()

// Обработка уведомлений
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := w.processPending(ctx); err != nil {
log.Printf("Process error: %v", err)
}
}
}
}

// processPending обрабатывает неотправленные уведомления
func (w *NotificationWorker) processPending(ctx context.Context) error {
// Получаем неотправленные уведомления
events, err := w.storage.GetPending(ctx, 100)
if err != nil {
return fmt.Errorf("get pending: %w", err)
}

for _, event := range events {
// Обрабатываем каждое уведомление
if err := w.processEvent(ctx, event); err != nil {
log.Printf("Process event %s error: %v", event.ID, err)
}
}

return nil
}

// processEvent обрабатывает одно уведомление
func (w *NotificationWorker) processEvent(ctx context.Context, event *NotificationEvent) error {
processor, ok := w.processors[event.MessageType]
if !ok {
return w.storage.MarkFailed(ctx, event.ID, fmt.Sprintf("unknown type: %s", event.MessageType))
}

// Отправляем уведомление
if err := processor.Send(ctx, event); err != nil {
// Увеличиваем счётчик попыток
if event.RetryCount >= event.MaxRetries {
return w.storage.MarkFailed(ctx, event.ID, err.Error())
}
return err // Повторим позже
}

// Помечаем как отправленное
return w.storage.MarkSent(ctx, event.ID)
}

10. Обработка зависших сообщений

package notification

import (
"context"
"log"
"time"
)

// StuckMessageDetector обнаруживает и обрабатывает зависшие сообщения
type StuckMessageDetector struct {
storage NotificationStorage
maxAge time.Duration
checkInterval time.Duration
}

func NewStuckMessageDetector(storage NotificationStorage) *StuckMessageDetector {
return &StuckMessageDetector{
storage: storage,
maxAge: 5 * time.Minute,
checkInterval: 1 * time.Minute,
}
}

// Start запускает детектор
func (d *StuckMessageDetector) Start(ctx context.Context) error {
ticker := time.NewTicker(d.checkInterval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := d.checkStuckMessages(ctx); err != nil {
log.Printf("Check stuck messages error: %v", err)
}
}
}
}

// checkStuckMessages проверяет зависшие сообщения
func (d *StuckMessageDetector) checkStuckMessages(ctx context.Context) error {
// Находим сообщения, которые слишком долго в статусе "sending"
stuckBefore := time.Now().Add(-d.maxAge)

// Обновляем статус на "pending" для повторной обработки
_, err := d.storage.(*PostgresStorage).db.ExecContext(ctx, `
UPDATE notifications
SET status = 'pending', retry_count = retry_count + 1
WHERE status = 'sending' AND updated_at < $1
`, stuckBefore)

return err
}

11. Паттерн Saga для уведомлений об отправке

package notification

// SagaOrchestrator координирует процесс отправки уведомлений
type SagaOrchestrator struct {
producer *EventProducer
}

// SendNotificationSaga запускает сагу отправки уведомления
func (s *SagaOrchestrator) SendNotificationSaga(ctx context.Context, event *NotificationEvent) error {
// Шаг 1: Отправляем событие в Kafka
if err := s.producer.SendNotification(ctx, event); err != nil {
return fmt.Errorf("step 1 - send to kafka: %w", err)
}

// Шаг 2: Ждём подтверждения обработки (через отдельный топик)
// Реализация через consumer топика "notifications-result"

return nil
}

// HandleResult обрабатывает результат отправки
func (s *SagaOrchestrator) HandleResult(ctx context.Context, result *NotificationResult) error {
switch result.Status {
case "sent":
// Успешно отправлено
log.Printf("Notification %s sent successfully", result.NotificationID)

case "failed":
// Ошибка — компенсирующее действие
log.Printf("Notification %s failed: %s", result.NotificationID, result.Error)

// Можно отправить альтернативное уведомление
// или уведомить администратора

case "partial":
// Частичная отправка (например, email отправлен, но SMS нет)
log.Printf("Notification %s partially sent", result.NotificationID)
}

return nil
}

type NotificationResult struct {
NotificationID string `json:"notification_id"`
Status string `json:"status"` // sent, failed, partial
Error string `json:"error,omitempty"`
Channels []string `json:"channels"` // Какие каналы были использованы
}

12. Мониторинг и метрики

package notification

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
notificationsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "notifications_total",
Help: "Total number of notifications",
}, []string{"type", "status"})

notificationLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "notification_latency_seconds",
Help: "Notification processing latency",
Buckets: prometheus.DefBuckets,
}, []string{"type"})

pendingNotifications = promauto.NewGauge(prometheus.GaugeOpts{
Name: "pending_notifications",
Help: "Number of pending notifications",
})

leaderElectionStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "leader_election_status",
Help: "Leader election status (1=leader, 0=follower)",
}, []string{"dc"})
)

// RecordNotification записывает метрику уведомления
func RecordNotification(notifType, status string) {
notificationsTotal.WithLabelValues(notifType, status).Inc()
}

// RecordLatency записывает латентность
func RecordLatency(notifType string, duration float64) {
notificationLatency.WithLabelValues(notifType).Observe(duration)
}

// UpdatePendingCount обновляет количество ожидающих
func UpdatePendingCount(count float64) {
pendingNotifications.Set(count)
}

Итог: Для гарантии exactly-once доставки в мульти-DC окружении необходимо: 1) Уникальный ID сообщения для дедупликации, 2) Атомарное сохранение в БД с ON CONFLICT, 3) Leader Election для исключения параллельной отправки, 4) Паттерн Saga для отслеживания результатов, 5) Обработка зависших сообщений с тайм-аутами, 6) Мониторинг и метрики для наблюдения за системой.

Вопрос 42. Что такое выравнивание памяти (memory alignment) в Go и как оно влияет на производительность?

Таймкод: 02:51:37

Ответ собеседника: Правильный. Выравнивание памяти — это когда поля структуры располагаются в памяти не последовательно, а с учётом размера процессорного слова. Например, если после bool (1 байт) идёт int64 (8 байт), компилятор может добавить 7 байт паддинга для выравнивания. Это влияет на размер структуры и производительность. В продуктовых командах такими оптимизациями обычно не заморачиваются — это не критично. Однако в инфраструктуре иногда смотрят на такие вещи, но в основном это актуально для низкоуровневого кода.

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

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

1. Что такое выравнивание памяти

┌─────────────────────────────────────────────────────────────────┐
│ Выравнивание памяти │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Без выравнивания (медленно): │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ B │ │ │ │ │ │ │ │ ← int64 не выровнен │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ▲ ▲ │
│ │←────── 2 чтения ──────────│ │
│ │
│ С выравниванием (быстро): │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ B │pad│pad│pad│pad│pad│pad│pad│ ← паддинг │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ int64 (8 байт) │ ← выровнен │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ▲ ▲ │
│ │←────────────── 1 чтение ───────────────────────────────│ │
│ │
└─────────────────────────────────────────────────────────────────┘

2. Правила выравнивания в Go

package main

import (
"fmt"
"unsafe"
)

// Пример структуры с неоптимальным порядком полей
type BadStruct struct {
A bool // 1 байт
B int64 // 8 байт
C bool // 1 байт
D int64 // 8 байт
}

// Пример структуры с оптимальным порядком полей
type GoodStruct struct {
B int64 // 8 байт
D int64 // 8 байт
A bool // 1 байт
C bool // 1 байт
}

func main() {
fmt.Println("BadStruct size:", unsafe.Sizeof(BadStruct{}))
// Output: 32 байта

fmt.Println("GoodStruct size:", unsafe.Sizeof(GoodStruct{}))
// Output: 24 байта

// Разница в 8 байт из-за паддинга!
}

3. Расположение полей в памяти

package main

import (
"fmt"
"unsafe"
)

type Example struct {
A bool
B int64
C bool
}

func main() {
e := Example{A: true, B: 42, C: true}

// Адреса полей
fmt.Printf("Address of A: %p\n", &e.A)
fmt.Printf("Address of B: %p\n", &e.B)
fmt.Printf("Address of C: %p\n", &e.C)

// Размеры
fmt.Printf("Size of Example: %d\n", unsafe.Sizeof(e))
fmt.Printf("Size of A: %d\n", unsafe.Sizeof(e.A))
fmt.Printf("Size of B: %d\n", unsafe.Sizeof(e.B))
fmt.Printf("Size of C: %d\n", unsafe.Sizeof(e.C))

// Offset полей
fmt.Printf("Offset of A: %d\n", unsafe.Offsetof(e.A))
fmt.Printf("Offset of B: %d\n", unsafe.Offsetof(e.B))
fmt.Printf("Offset of C: %d\n", unsafe.Offsetof(e.C))
}

4. Визуализация паддинга

┌─────────────────────────────────────────────────────────────────┐
│ struct { A bool; B int64; C bool } │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Адрес: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ A │ pad │ pad │ pad │ pad │ pad │ pad │ pad │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ 1B 1B 1B 1B 1B 1B 1B 1B │
│ │
│ Адрес: 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ B (int64) │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ 8 байт — выровнен по 8-байтной границе │
│ │
│ Адрес: 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ C │ pad │ pad │ pad │ pad │ pad │ pad │ pad │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ 1B 1B 1B 1B 1B 1B 1B 1B │
│ │
│ Итого: 24 байта вместо 10 байт (1 + 8 + 1) │
│ Паддинг: 14 байт (7 после A + 7 после C) │
│ │
└─────────────────────────────────────────────────────────────────┘

5. Оптимизация размера структуры

package main

import (
"fmt"
"unsafe"
)

// Неоптимальный порядок — 24 байта
type Unoptimized struct {
A bool // 1 байт + 7 байт паддинга
B int64 // 8 байт
C bool // 1 байт + 7 байт паддинга
D int32 // 4 байта + 4 байта паддинга
}

// Оптимальный порядок — 16 байт
type Optimized struct {
B int64 // 8 байт
D int32 // 4 байта
A bool // 1 байт
C bool // 1 байт + 2 байта паддинга
}

func main() {
fmt.Println("Unoptimized size:", unsafe.Sizeof(Unoptimized{}))
// Output: 24

fmt.Println("Optimized size:", unsafe.Sizeof(Optimized{}))
// Output: 16

// Экономия: 8 байт (33%)
}

6. Правило оптимального порядка полей

package main

import "unsafe"

// Правило: располагайте поля по убыванию размера
// От большего к меньшему

// Плохо — случайный порядок
type BadOrder struct {
Flag1 bool // 1 байт
ID int64 // 8 байт
Flag2 bool // 1 байт
Count int32 // 4 байта
Flag3 bool // 1 байт
Value float64 // 8 байт
}
// Размер: 40 байт

// Хорошо — по убыванию размера
type GoodOrder struct {
ID int64 // 8 байт
Value float64 // 8 байт
Count int32 // 4 байта
Flag1 bool // 1 байт
Flag2 bool // 1 байт
Flag3 bool // 1 байт
// + 1 байт паддинга для выравнивания всей структуры
}
// Размер: 24 байта

func main() {
fmt.Println("BadOrder:", unsafe.Sizeof(BadOrder{})) // 40
fmt.Println("GoodOrder:", unsafe.Sizeof(GoodOrder{})) // 24
}

7. Влияние на производительность

package main

import (
"testing"
"unsafe"
)

// Тест производительности с кэш-линиями
type CacheLineStruct struct {
Data [64]byte // Ровно одна кэш-линия (обычно 64 байта)
}

type FalseSharingStruct struct {
Counter1 int64 // 8 байт
Counter2 int64 // 8 байт — может быть на той же кэш-линии!
}

// Benchmark для демонстрации false sharing
func BenchmarkFalseSharing(b *testing.B) {
var fs FalseSharingStruct

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
fs.Counter1++
}
})
}

func BenchmarkNoFalseSharing(b *testing.B) {
type PaddedCounter struct {
Counter int64
_ [56]byte // Паддинг до 64 байт
}

var c PaddedCounter

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Counter++
}
})
}

8. Кэш-линии и false sharing

┌─────────────────────────────────────────────────────────────────┐
│ False Sharing Problem │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Кэш-линия (64 байта): │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Counter1 (8B) │ Counter2 (8B) │ ... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ CPU 1 изменяет CPU 2 изменяет │
│ Counter1 Counter2 │
│ │
│ Проблема: Оба счётчика на одной кэш-линии! │
│ → CPU 1 инвалидирует кэш-линию для CPU 2 │
│ → CPU 2 инвалидирует кэш-линию для CPU 1 │
│ → Постоянная синхронизация кэшей │
│ │
│ Решение: Паддинг между счётчиками │
│ ┌───────────────────────┬───────────────────────┐ │
│ │ Counter1 (8B) │ 56B │ Counter2 (8B) │ 56B │ │
│ └───────────────────────┴───────────────────────┘ │
│ Кэш-линия 1 Кэш-линия 2 │
│ │
└─────────────────────────────────────────────────────────────────┘

9. Практический пример: оптимизация структуры заказа

package main

import (
"fmt"
"unsafe"
)

// До оптимизации — 64 байта
type OrderBefore struct {
IsActive bool // 1 байт + 7 байт паддинга
TotalAmount float64 // 8 байт
IsPaid bool // 1 байт + 3 байта паддинга
ItemCount int32 // 4 байта
OrderID int64 // 8 байт
IsShipped bool // 1 байт + 7 байт паддинга
Discount float64 // 8 байт
IsCancelled bool // 1 байт + 3 байта паддинга
Quantity int32 // 4 байта
}

// После оптимизации — 40 байт
type OrderAfter struct {
TotalAmount float64 // 8 байт
Discount float64 // 8 байт
OrderID int64 // 8 байт
ItemCount int32 // 4 байта
Quantity int32 // 4 байта
IsActive bool // 1 байт
IsPaid bool // 1 байт
IsShipped bool // 1 байт
IsCancelled bool // 1 байт
// + 4 байта паддинга для выравнивания
}

func main() {
fmt.Println("OrderBefore:", unsafe.Sizeof(OrderBefore{})) // 64
fmt.Println("OrderAfter:", unsafe.Sizeof(OrderAfter{})) // 40

// Экономия: 24 байта (37.5%)

// При 1 млн заказов в памяти:
// До: 64 MB
// После: 40 MB
// Экономия: 24 MB!
}

10. Инструменты для анализа

# Проверка размера структуры
go run -gcflags="-m" main.go

# Анализ выравнивания
go vet -shadow=true ./...

# Бенчмарки
go test -bench=. -benchmem

# Профилирование
go test -cpuprofile=cpu.prof -memprofile=mem.prof
go tool pprof cpu.prof

11. Когда это важно

// 1. Высоконагруженные системы
type HighLoadStruct struct {
// Оптимизация критична
}

// 2. Большие массивы структур
type SmallStruct struct {
ID int64 // 8 байт
Flag bool // 1 байт
}

func processMillions() {
// 10 млн структур
data := make([]SmallStruct, 10_000_000)

// Без оптимизации: ~160 MB
// С оптимизацией: ~90 MB
// Экономия: 70 MB!
}

// 3. Критичные по производительности участки кода
// 4. Системы реального времени
// 5. Встраиваемые системы с ограниченной памятью

12. Когда это НЕ важно

// 1. Обычные бизнес-приложения
type User struct {
Name string
Email string
Age int
}

// 2. Редко создаваемые объекты
type Config struct {
Host string
Port int
Timeout time.Duration
}

// 3. Когда читаемость важнее производительности
// 4. Прототипы и MVP

Итог: Выравнивание памяти — это механизм размещения данных по адресам, кратным их размеру. В Go компилятор автоматически добавляет паддинг между полями структур. Оптимизация порядка полей (от большего к меньшему) может уменьшить размер структуры на 30-50%. Это важно для высоконагруженных систем, больших массивов структур и критичных по производительности участков кода. Для обычных бизнес-приложений оптимизация выравнивания обычно не критична.

Вопрос 43. Почему нельзя всё хранить в куче вместо стека, и в чём разница между стеком и кучей?

Таймкод: 02:53:19

Ответ собеседника: Правильный. Стек — это область памяти, которая управляется автоматически: локальные переменные создаются при вызове функции и удаляются при её завершении. Стек работает очень быстро (LIFO), но ограничен по размеру. Куча (heap) — динамическая память, выделяется вручную (make, new в Go) и управляется сборщиком мусора. В куче можно хранить большие объёмы данных и данные с неизвестным временем жизни, но доступ к ней медленнее из-за необходимости аллокации и сборки мусора. Нельзя всё хранить в куче, потому что: 1) Стек быстрее — аллокация это просто сдвиг указателя. 2) Стек автоматически освобождает память — нет нагрузки на GC. 3) Стек ограничен по размеру (обычно 1-8 МБ на горутину в Go), поэтому большие данные всё равно идут в кучу.

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

Стек и куча — два принципиально разных способа управления памятью с разными характеристиками производительности и временем жизни данных.

1. Сравнение стека и кучи

┌─────────────────────────────────────────────────────────────────────────────┐
│ Стек vs Куча │
├─────────────────────────────┬───────────────────────────────────────────────┤
│ Стек │ Куча │
├─────────────────────────────┼───────────────────────────────────────────────┤
│ Автоматическое управление │ Ручное управление (GC в Go) │
│ LIFO (Last In First Out) │ Произвольный порядок │
│ Очень быстрая аллокация │ Медленная аллокация (поиск свободного блока) │
│ Освобождение автоматическое │ Освобождение через GC │
│ Ограниченный размер │ Большой размер (ограничен RAM) │
│ Данные живут до конца ф-ции │ Данные живут до сборки мусора │
│ Нет фрагментации │ Возможна фрагментация │
│ Потокобезопасен по умолчанию│ Требует синхронизации │
└─────────────────────────────┴───────────────────────────────────────────────┘

2. Как работает стек

package main

import "fmt"

func main() {
// Все эти переменные размещаются в стеке
x := 10 // 8 байт в стеке
y := 20 // 8 байт в стеке
name := "Go" // указатель в стеке, данные в статической памяти

result := add(x, y)
fmt.Println(result)
}

func add(a, b int) int {
// a, b, result — все в стеке
result := a + b
return result
// При выходе из функции стековый фрейм автоматически освобождается
}

3. Визуализация стека

┌─────────────────────────────────────────────────────────────────┐
│ Стек вызовов │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Адрес │ Содержимое │
│ ──────┼──────────────────────────────────────────────── │
│ 0x1000│ ┌─────────────────────────────────────────────┐ │
│ │ │ main() │ │
│ │ │ x = 10 │ │
│ │ │ y = 20 │ │
│ │ │ name = "Go" │ │
│ │ │ result = 30 │ │
│ │ └─────────────────────────────────────────────┘ │
│ │ ┌─────────────────────────────────────────────┐ │
│ │ │ add(10, 20) │ │
│ │ │ a = 10 │ │
│ │ │ b = 20 │ │
│ │ │ result = 30 │ │
│ │ └─────────────────────────────────────────────┘ ← SP │
│ 0x0FFF│ (свободная память) │
│ │ │
│ ↓ Стек растёт вниз (к меньшим адресам) │
│ │
│ SP (Stack Pointer) — указатель на вершину стека │
│ │
└─────────────────────────────────────────────────────────────────┘

4. Как работает куча

package main

import "fmt"

func main() {
// Эта переменная размещается в куче
ptr := createOnHeap()
fmt.Println(*ptr)
// GC освободит память, когда ptr станет недоступен
}

func createOnHeap() *int {
// Go анализирует, что указатель возвращается из функции
// → размещает x в куче (escape analysis)
x := 42
return x // x "убегает" в кучу
}

5. Escape Analysis в Go

package main

// Компилятор Go использует escape analysis для определения,
// где разместить переменную: в стеке или куче

// Пример 1: Переменная остаётся в стеке
func stackExample() int {
x := 42
return x // Возвращаем значение, не указатель
}

// Пример 2: Переменная "убегает" в кучу
func heapExample() *int {
x := 42
return &x // Возвращаем указатель → x в куче
}

// Пример 3: Переменная убегает через интерфейс
func interfaceExample() interface{} {
x := 42
return x // Интерфейс → x в куче
}

// Пример 4: Переменная убегает через замыкание
func closureExample() func() int {
x := 42
return func() int {
return x // Замыкание захватывает x → x в куче
}
}

// Проверка escape analysis:
// go build -gcflags="-m" main.go

6. Почему нельзя всё хранить в куче

package main

import (
"testing"
)

// Бенчмарк: стек vs куча
func BenchmarkStack(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 42 // Стек
_ = x + 1
}
}

func BenchmarkHeap(b *testing.B) {
for i := 0; i < b.N; i++ {
x := new(int) // Куча
*x = 42
_ = *x + 1
}
}

// Результат:
// BenchmarkStack-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
// BenchmarkHeap-8 50000000 25.0 ns/op 8 B/op 1 allocs/op
//
// Стек в 100 раз быстрее!

7. Нагрузка на сборщик мусора

package main

import (
"runtime"
"time"
)

func main() {
// Если всё в куче → GC работает постоянно
// Если часть в стеке → GC работает реже

var stats runtime.MemStats

// Создаём много объектов в куче
for i := 0; i < 1000000; i++ {
_ = make([]byte, 1024) // Каждый раз аллокация в куче
}

runtime.ReadMemStats(&stats)
fmt.Printf("GC runs: %d\n", stats.NumGC)
fmt.Printf("Total GC pause: %d ms\n", stats.PauseTotalNs/1e6)

// При использовании стека:
// - Нет аллокаций
// - Нет работы GC
// - Нет пауз на сборку мусора
}

8. Размер стека в Go

package main

import (
"fmt"
"runtime"
)

func main() {
// В Go каждый горутине выделяется свой стек
// Начальный размер: 2 KB (Go 1.4+)
// Максимальный размер: 1 GB

// Стек автоматически растёт при необходимости
// Но это требует копирования данных!

fmt.Println("Goroutine stack size:", runtime.Stack(nil, false))
}

// Пример переполнения стека
func recursiveOverflow(n int) {
var buffer [1024]byte // 1 KB на каждый вызов
fmt.Printf("Depth: %d, buffer addr: %p\n", n, &buffer)
recursiveOverflow(n + 1) // Бесконечная рекурсия
// runtime: goroutine stack exceeds 1 GB limit
// fatal error: stack overflow
}

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

package main

// Используйте СТЕК когда:
// 1. Размер данных известен на этапе компиляции
// 2. Данные нужны только внутри функции
// 3. Нужна максимальная производительность

func stackUsage() {
var arr [100]int // Массив фиксированного размера → стек
x := 42 // Простая переменная → стек
point := struct{ X, Y float64 }{1.0, 2.0} // Маленькая структура → стек
_, _ = arr, x
}

// Используйте КУЧУ когда:
// 1. Размер данных неизвестен на этапе компиляции
// 2. Данные должны пережить вызов функции
// 3. Нужно передать владение данными
// 4. Очень большие данные (больше стека)

func heapUsage() {
slice := make([]int, 1000) // Динамический размер → куча
m := make(map[string]int) // Map → куча
ch := make(chan int) // Channel → куча
ptr := new(int) // Явное выделение → куча
_, _, _ = slice, m, ch
_ = ptr
}

10. Оптимизация: уменьшение аллокаций в куче

package main

import "fmt"

// Плохо: каждая итерация создаёт объект в куче
func processItemsBad(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
result := processItem(item) // Возвращает *Result → куча
results = append(results, *result)
}
return results
}

// Хорошо: используем стек, возвращая значение
func processItemsGood(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
result := processItemOnStack(item) // Возвращает Result → стек
results = append(results, result)
}
return results
}

type Item struct {
ID int
Name string
}

type Result struct {
ID int
Status string
}

func processItem(item Item) *Result {
return &Result{ID: item.ID, Status: "processed"} // Куча!
}

func processItemOnStack(item Item) Result {
return Result{ID: item.ID, Status: "processed"} // Стек!
}

11. Синхронизация и потокобезопасность

package main

import "sync"

// Стек потокобезопасен по умолчанию
func stackIsSafe() {
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
x := id * 2 // Каждая горутина имеет свой стек
_ = x
}(i)
}

wg.Wait()
}

// Куча требует синхронизации
type SharedCounter struct {
mu sync.Mutex
count int // В куче, доступ из разных горутин
}

func (c *SharedCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}

12. Итоговая таблица

┌─────────────────────────────────────────────────────────────────────────────┐
│ Стек vs Куча: Итог │
├─────────────────────────────┬───────────────────────────────────────────────┤
│ Критерий │ Стек │ Куча │
├─────────────────────────────┼────────────────┼─────────────────────────────┤
│ Скорость аллокации │ ~1 ns │ ~25-100 ns │
│ Скорость доступа │ Быстрый │ Медленнее (кэш-промахи) │
│ Освобождение │ Автоматическое │ GC (паузы) │
│ Размер │ 2KB-1GB │ До размера RAM │
│ Потокобезопасность │ Да │ Нет (нужна синхронизация) │
│ Фрагментация │ Нет │ Да │
│ Нагрузка на GC │ Нет │ Да │
│ Время жизни │ Функция │ До сборки мусора │
│ Использование │ По умолчанию │ При необходимости │
└─────────────────────────────┴────────────────┴─────────────────────────────┘

Итог: Стек и куча — взаимодополняющие механизмы. Стек быстрее, автоматически управляется и не нагружает GC, но ограничен по размеру и времени жизни данных. Куча позволяет хранить большие объёмы данных с гибким временем жизни, но требует работы GC и медленнее при аллокации. Go автоматически решает, где разместить переменную (escape analysis), но понимание разницы помогает писать более эффективный код.

Вопрос 44. Используют ли дженерики в Go в продакшене и какие есть лучшие практики их применения?

Таймкод: 02:53:39

Ответ собеседника: Правильный. Дженерики в Go появились в версии 1.18. В крупных компаниях используются не самые свежие версии Go — обновления приходят с некоторым запозданием, чтобы сначала вычистить возможные ошибки и убедиться в стабильности. Дженерики уже начинают использоваться, но не повсеместно. Основная проблема — линтеры (golangci-lint и другие) ещё не полностью адаптированы под дженерики, поэтому сначала нужно починить линтеры, а потом уже активно внедрять дженерики в основной код.

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

Дженерики в Go появились в версии 1.18 (март 2022) и уже активно используются в продакшене. Вот полное руководство по их применению.

1. Базовый синтаксис дженериков

package main

import "fmt"

// Функция с дженериком
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}

// Структура с дженериком
type Stack[T any] struct {
items []T
}

func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}

func main() {
fmt.Println(Min(1, 2)) // 1
fmt.Println(Min(1.5, 2.5)) // 1.5
fmt.Println(Min("a", "b")) // "a"

stack := Stack[int]{}
stack.Push(1)
stack.Push(2)
fmt.Println(stack.Pop()) // 2, true
}

2. Ограничения (Constraints)

package main

import (
"fmt"
"golang.org/x/exp/constraints"
)

// Встроенные constraints
func Sum[T constraints.Integer | constraints.Float](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}

// Собственный constraint
type Stringer interface {
String() string
}

type Printable interface {
~string | ~int | ~float64
Stringer
}

func Print[T Printable](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}

// Union constraint
type Number interface {
~int | ~int64 | ~float64
}

func Double[T Number](n T) T {
return n * 2
}

3. Практические примеры использования

package main

import "fmt"

// 1. Map для срезов
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}

// 2. Filter для срезов
func Filter[T any](slice []T, fn func(T) bool) []T {
result := make([]T, 0, len(slice))
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}

// 3. Reduce для срезов
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}

// 4. Optional/Maybe тип
type Optional[T any] struct {
value T
ok bool
}

func Some[T any](value T) Optional[T] {
return Optional[T]{value: value, ok: true}
}

func None[T any]() Optional[T] {
return Optional[T]{ok: false}
}

func (o Optional[T]) Get() (T, bool) {
return o.value, o.ok
}

func (o Optional[T]) OrElse(defaultValue T) T {
if o.ok {
return o.value
}
return defaultValue
}

func main() {
// Map example
nums := []int{1, 2, 3, 4, 5}
strings := Map(nums, func(n int) string {
return fmt.Sprintf("num: %d", n)
})
fmt.Println(strings) // [num: 1 num: 2 num: 3 num: 4 num: 5]

// Filter example
evens := Filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4]

// Reduce example
sum := Reduce(nums, 0, func(acc, n int) int {
return acc + n
})
fmt.Println(sum) // 15

// Optional example
opt := Some(42)
if val, ok := opt.Get(); ok {
fmt.Println("Value:", val)
}

empty := None[int]()
fmt.Println(empty.OrElse(0)) // 0
}

4. Репозиторий с дженериками

package main

import (
"context"
"fmt"
)

// Базовые интерфейсы
type Entity interface {
GetID() int64
}

type Repository[T Entity] interface {
FindByID(ctx context.Context, id int64) (T, error)
FindAll(ctx context.Context) ([]T, error)
Create(ctx context.Context, entity T) error
Update(ctx context.Context, entity T) error
Delete(ctx context.Context, id int64) error
}

// Реализация для User
type User struct {
ID int64
Name string
}

func (u User) GetID() int64 {
return u.ID
}

type UserRepo struct {
// db *sql.DB
}

func (r *UserRepo) FindByID(ctx context.Context, id int64) (User, error) {
// Реализация запроса к БД
return User{ID: id, Name: "John"}, nil
}

func (r *UserRepo) FindAll(ctx context.Context) ([]User, error) {
return []User{{ID: 1, Name: "John"}}, nil
}

func (r *UserRepo) Create(ctx context.Context, user User) error {
fmt.Printf("Creating user: %s\n", user.Name)
return nil
}

func (r *UserRepo) Update(ctx context.Context, user User) error {
fmt.Printf("Updating user: %d\n", user.ID)
return nil
}

func (r *UserRepo) Delete(ctx context.Context, id int64) error {
fmt.Printf("Deleting user: %d\n", id)
return nil
}

// Обобщённый сервис
type Service[T Entity] struct {
repo Repository[T]
}

func (s *Service[T]) GetByID(ctx context.Context, id int64) (T, error) {
return s.repo.FindByID(ctx, id)
}

func main() {
ctx := context.Background()

userRepo := &UserRepo{}
userService := Service[User]{repo: userRepo}

user, _ := userService.GetByID(ctx, 1)
fmt.Println(user)
}

5. Кэш с дженериками

package main

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

type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]cacheEntry[V]
ttl time.Duration
}

type cacheEntry[V any] struct {
value V
expiresAt time.Time
}

func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
cache := &Cache[K, V]{
items: make(map[K]cacheEntry[V]),
ttl: ttl,
}
go cache.cleanup()
return cache
}

func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = cacheEntry[V]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

entry, ok := c.items[key]
if !ok || time.Now().After(entry.expiresAt) {
var zero V
return zero, false
}
return entry.value, true
}

func (c *Cache[K, V]) cleanup() {
ticker := time.NewTicker(c.ttl)
for range ticker.C {
c.mu.Lock()
now := time.Now()
for key, entry := range c.items {
if now.After(entry.expiresAt) {
delete(c.items, key)
}
}
c.mu.Unlock()
}
}

func main() {
// Кэш для пользователей
userCache := NewCache[int64, string](5 * time.Minute)
userCache.Set(1, "John")
userCache.Set(2, "Jane")

if name, ok := userCache.Get(1); ok {
fmt.Println("User 1:", name)
}

// Кэш для сессий
sessionCache := NewCache[string, map[string]any](30 * time.Minute)
sessionCache.Set("session-123", map[string]any{
"user_id": 1,
"role": "admin",
})
}

6. Worker Pool с дженериками

package main

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

type Job[T any] struct {
Payload T
Result chan JobResult[T]
}

type JobResult[T any] struct {
Value T
Error error
}

type WorkerPool[T any, R any] struct {
numWorkers int
jobChan chan Job[T]
handler func(T) (R, error)
}

func NewWorkerPool[T any, R any](numWorkers int, handler func(T) (R, error)) *WorkerPool[T, R] {
return &WorkerPool[T, R]{
numWorkers: numWorkers,
jobChan: make(chan Job[T], numWorkers*2),
handler: handler,
}
}

func (wp *WorkerPool[T, R]) Start(ctx context.Context) {
var wg sync.WaitGroup

for i := 0; i < wp.numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job := <-wp.jobChan:
result, err := wp.handler(job.Payload)
job.Result <- JobResult[T]{Value: result, Error: err}
}
}
}()
}

go func() {
wg.Wait()
}()
}

func (wp *WorkerPool[T, R]) Submit(payload T) JobResult[T] {
resultChan := make(chan JobResult[T], 1)
wp.jobChan <- Job[T]{Payload: payload, Result: resultChan}
return <-resultChan
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Worker pool для обработки заказов
orderPool := NewWorkerPool[int64, string](5, func(orderID int64) (string, error) {
return fmt.Sprintf("Order %d processed", orderID), nil
})

orderPool.Start(ctx)

result := orderPool.Submit(123)
fmt.Println(result.Value)
}

7. Лучшие практики использования дженериков

package main

import (
"fmt"
"golang.org/x/exp/constraints"
)

// ✅ ХОРОШО: Используйте дженерики для утилитарных функций
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}

// ✅ ХОРОШО: Ограничивайте типы, когда это необходимо
func Sum[T constraints.Ordered](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}

// ❌ ПЛОХО: Не используйте дженерики, когда хватает интерфейсов
// Плохо:
func ProcessBad[T fmt.Stringer](items []T) {
for _, item := range items {
_ = item.String()
}
}

// Хорошо:
func ProcessGood(items []fmt.Stringer) {
for _, item := range items {
_ = item.String()
}
}

// ❌ ПЛОХО: Не усложняйте без необходимости
// Плохо:
func Overcomplicated[A constraints.Integer, B constraints.Float, C any](a A, b B, C C) C {
return c
}

// ✅ ХОРОШО: Используйте понятные имена для типовых параметров
type ID interface {
~int | ~int64 | ~string
}

type Entity[IDType ID] struct {
ID IDType
Name string
}

// ✅ ХОРОШО: Композиция интерфейсов с дженериками
type Repository[IDType ID, T Entity[IDType]] interface {
FindByID(id IDType) (T, error)
Save(entity T) error
}

func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Sum([]int{1, 2, 3})) // 6
}

8. Когда использовать дженерики, а когда интерфейсы

┌─────────────────────────────────────────────────────────────────────────────┐
│ Дженерики vs Интерфейсы │
├─────────────────────────────────┬───────────────────────────────────────────┤
│ Используйте дженерики │ Используйте интерфейсы │
├─────────────────────────────────┼───────────────────────────────────────────┤
│ Работа с разными типами данных │ Работа с поведением (методы) │
│ без потери типизации │ │
│ Утилитарные функции (Map, │ Полиморфизм во время выполнения │
│ Filter, Reduce) │ │
│ Контейнеры (Stack, Queue, │ Зависимости в бизнес-логике │
│ Cache, Pool) │ │
│ Когда нужна производительность │ Когда нужна гибкость │
│ (без boxing/unboxing) │ │
│ Типовые параметры известны │ Тип определяется во время выполнения │
│ на этапе компиляции │ │
└─────────────────────────────────┴───────────────────────────────────────────┘

Итог: Дженерики в Go — мощный инструмент, который уже активно используется в продакшене. Они особенно полезны для утилитарных функций, контейнеров данных и обобщённых репозиториев. Однако не стоит использовать дженерики везде — интерфейсы по-прежнему лучше подходят для полиморфизма и работы с поведением.