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

Go Middle 1 Roadmap (2025-2026) — подготовка к собеседованию в РФ/СНГ

Источники собраны из официальной документации Go, статей Хабра, блогов 2025-2026, материалов от Яндекс.Практикум, Tinkoff Career, Ozon Tech и реальных собеседований в Avito, VK, Сбер. Roadmap рассчитан на грейд Middle 1 (junior+ → начальный middle): уровень, где от тебя ждут самостоятельной работы, понимания “что под капотом” и опыта в production-задачах.


По данным анализа 9 247 технических интервью в 2025-2026, Go стал самым быстрорастущим стеком — доля интервью выросла с 7% (Q1) до 12% (Q4), Go вышел на 4-е место после Python, Java и C#. Главные работодатели — Ozon, Avito, Яндекс, T-Bank, VK, Сбер.

Особенности Go-интервью:

  • System Design встречается в 34% случаев (выше среднего) — Go берут под high-load.
  • SOLID спрашивают реже (21%), фокус — на конкурентности, каналах, API-дизайне.
  • Внутреннее устройство языка (slice, map, channel, scheduler, GC) — обязательный минимум.
  • Реальные задачи на race conditions — must.

Что отличает Middle 1 от Junior:

  • Junior: пишет код, знает синтаксис, делает CRUD-сервисы.
  • Middle 1: понимает КАК работают горутины, GC, escape analysis; умеет писать тесты, профилировать, дебажить production; знает паттерны concurrency; работал с реальной БД и observability.
  • Middle 2/Senior: проектирует системы, может оптимизировать под нагрузку, отвечает за архитектуру.

В Go две внутренние структуры (runtime/iface.go):

// Непустой интерфейс
type iface struct {
tab *itab // указатель на таблицу методов
data unsafe.Pointer // указатель на данные
}
// Пустой интерфейс (interface{} / any)
type eface struct {
_type *_type // информация о типе
data unsafe.Pointer // данные
}
// itab — interface table
type itab struct {
inter *interfacetype // тип интерфейса
_type *_type // конкретный тип
hash uint32 // копия _type.hash (для type switch)
_ [4]byte
fun [1]uintptr // методы конкретного типа в порядке интерфейса
}

Почему две структуры: eface — оптимизация. При работе с any/interface{} не нужна таблица методов, поэтому экономим pointer load при type switch.

Важные следствия:

  • Любое присваивание конкретного типа в интерфейс — это 2 указателя в памяти.
  • itab кэшируется в runtime по паре (interfacetype, _type).
  • Сравнение интерфейсов = сравнение пар (tab/_type, data) → может паниковать, если data не comparable (map, slice, func).
  • nil interface ≠ interface с nil-указателем внутри:
var p *MyError = nil
var err error = p
fmt.Println(err == nil) // false! tab != nil, хотя data == nil

Это классический вопрос на собеседовании.

// Одиночное assertion: панические при неудаче
s := i.(string)
// Comma-ok: безопасное
s, ok := i.(string)
if !ok { /* ... */ }
// Type switch
switch v := i.(type) {
case nil:
// ...
case int:
fmt.Println(v * 2)
case string:
fmt.Println(len(v))
case Stringer: // assertion на интерфейс
fmt.Println(v.String())
default:
fmt.Printf("unknown: %T\n", v)
}

В type switch для каждой пары (исходный тип, target тип) runtime ищет itab в кэше. Это быстрее, чем reflect.

type Animal struct {
Name string
}
func (a Animal) Greet() string { return "Hi, I'm " + a.Name }
type Dog struct {
Animal // embedding
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Husky"}
fmt.Println(d.Greet()) // "Hi, I'm Rex" — метод поднялся
fmt.Println(d.Name) // "Rex" — поле тоже

Особенности:

  • Можно встраивать структуры, интерфейсы, указатели.
  • Не наследование: нет полиморфизма по embedded типу, нет super.
  • Конфликт имён: явное обращение d.Animal.Name.
  • Интерфейс может встраивать другие интерфейсы (io.ReadWriter = io.Reader + io.Writer).

Базовый синтаксис:

type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}

Когда использовать (по официальному гайду “When to use generics”):

  1. Функции работают со стандартными контейнерами (slice, map, channel) — Map, Filter, Reduce.
  2. Общие структуры данных (Stack[T], Cache[K, V], LinkedList[T]).
  3. Когда метод вызывается на разных типах, но поведение идентично.

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

  • Если нужно вызвать метод на значении — используй интерфейс, а не generic.
  • Когда реализации для разных типов реально разные.
  • Преждевременно: начинай с конкретных типов и рефактори в generic только когда повторение очевидно.

Go 1.21 → 1.24: улучшено type inference, type aliases с параметрами, range over functions/integers. Go 1.25 убрал концепцию “core types” — упростил спецификацию.

Стоит, если:

  • ORM/маппинг (sqlx, GORM, encoding/json).
  • Валидация по тегам (go-playground/validator).
  • DI-контейнеры (fx, dig).
  • Generic сериализация.

НЕ стоит, если:

  • Можно решить интерфейсом или generic.
  • В hot path — рефлексия медленнее в 10-100 раз.
v := reflect.ValueOf(user)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
tag := f.Tag.Get("json")
fmt.Printf("%s -> %v (json: %s)\n", f.Name, v.Field(i), tag)
}

Правила Лоуса о рефлексии: 1) reflection обратима, 2) интерфейс ↔ reflect.Value, 3) изменения требуют Addr() или передачи указателя.

unsafe.Pointer нарушает безопасность типов и должен использоваться очень осторожно. Применяется в:

  • CGO: взаимодействие с C-кодом.
  • Оптимизация без копирования: string ↔ []byte без аллокаций.
  • Доступ к приватным полям (тестирование, ORM).
  • Чтение бинарных форматов напрямую из памяти.
// Преобразование string в []byte без копирования (Go 1.20+ — есть unsafe.StringData/Slice)
b := unsafe.Slice(unsafe.StringData(s), len(s))

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

Компилятор Go сам решает, где разместить переменную. Если он может доказать, что значение не переживёт функцию — оно идёт на стек (дёшево, без GC). Если нет — escape to heap.

Что вызывает escape:

  1. Возврат указателя на локальную переменную:
func newInt() *int { x := 42; return &x } // x escape
  1. Конвертация в interface{}:
fmt.Println(x) // x теряет тип → eface → heap
  1. Замыкания, ловящие переменную по ссылке.
  2. Слайс/map, который растёт за пределы статически известного размера.
  3. Слишком большой объект для стека.

Проверка:

Окно терминала
go build -gcflags="-m" main.go
go build -gcflags="-m -m" main.go # больше деталей
# ./main.go:5:6: moved to heap: x

Базовые правила синхронизации:

  • В одной горутине операции упорядочены (program order).
  • Между горутинами порядок определяется только synchronization events: каналы, mutex, atomic, sync.Once, runtime.Gosched/Goexit.
  • Рецепт: всякое чтение, которое не синхронизировано с записью — это data race, и поведение не определено.

Конкретные гарантии:

  1. Отправка в канал happens-before приёма из канала.
  2. Закрытие канала happens-before приёма zero-value.
  3. n-й Unlock happens-before (n+1)-й Lock.
  4. atomic.Store happens-before atomic.Load, читающего записанное значение.

type hchan struct {
qcount uint // кол-во элементов в буфере
dataqsiz uint // capacity
buf unsafe.Pointer // кольцевой буфер
elemsize uint16
closed uint32
elemtype *_type
sendx uint // индекс для send в буфер
recvx uint // индекс для recv
recvq waitq // список goroutines, ждущих recv (sudog)
sendq waitq // список goroutines, ждущих send
lock mutex // защищает hchan
}

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

  1. Send в небуферизованный канал: если в recvq есть waiter → данные копируются напрямую goroutine → goroutine, отправитель продолжает. Если нет — отправитель блокируется (создаётся sudog, push в sendq, паркуется).
  2. Send в буферизованный: если есть место в buf → копия в buf[sendx]. Иначе — блокируется.
  3. Recv: симметрично.

Lock — hybrid spin-mutex для уменьшения overhead.

Каждый case → scase{c *hchan, kind, elem}. Алгоритм select:

  1. Random shuffle порядка cases (через fastrandn) — чтобы избежать starvation.
  2. Сортировка по lock-order (адрес channel) для избежания deadlock при многоканальном lock.
  3. Заблокировать все каналы.
  4. Пройти cases, если есть ready — выполнить и unlock все.
  5. Если нет — для каждого канала создать sudog и записать в его recvq/sendq, потом goroutine паркуется.
  6. Когда одна из операций готова — goroutine просыпается, остальные sudog удаляются.

Если есть default — он используется, если ни один case не готов сразу (без блокировки).

ТипКогда использовать
sync.MutexЗащита данных, < few μs критическая секция.
sync.RWMutexМного чтений, мало записей. Под высокой нагрузкой может быть хуже Mutex из-за внутренней сложности.
sync.OnceОднократная инициализация (singleton, lazy init). Гарантирует happens-before.
sync.WaitGroupДождаться завершения N горутин. Add → перед Go(), Done в defer, Wait после.
sync.CondКоординация через condition variables. Чаще лучше каналы. Использовать когда нужен Broadcast многим waiters.
sync.PoolПереиспользование короткоживущих объектов (буферы, JSON encoders). GC может очистить pool в любой момент.
sync.MapConcurrent map для случаев: 1) ключ пишется один раз, читается много; 2) горутины работают с непересекающимися ключами. В общем случае Mutex + map быстрее.

Пример sync.Pool:

var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func handler(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().(*bytes.Buffer)
defer func() { buf.Reset(); bufPool.Put(buf) }()
// используем buf
}

Пакет sync/atomic — для примитивных типов (int32/64, uint32/64, uintptr, Pointer) и обёрток (atomic.Int64, atomic.Value, atomic.Bool — с Go 1.19).

var counter atomic.Int64
counter.Add(1)
v := counter.Load()

Правило выбора atomic vs Mutex:

  • Один примитив, простая операция (Add/Load/CAS) → atomic.
  • Несколько связанных полей или сложная логика → Mutex.

Правила использования:

  1. Context — первый параметр функции: func F(ctx context.Context, ...).
  2. Никогда не храни context в struct (исключения — для долгоживущих сервисов, явно документируй).
  3. Никогда не передавай nil context — используй context.TODO() если не уверен.
  4. Всегда defer cancel() для WithTimeout/WithDeadline/WithCancel.
  5. context.Value — только для request-scoped данных (trace ID, user ID), не для optional аргументов.
  6. Ключ для Value — свой неэкспортируемый тип:
type ctxKey int
const userIDKey ctxKey = 0
ctx = context.WithValue(ctx, userIDKey, 42)
v, ok := ctx.Value(userIDKey).(int)

Go 1.20+ ввёл context.WithCancelCause(ctx, err) — можно отменять с причиной, потом context.Cause(ctx) достаёт её.

Worker pool — ограничить параллелизм:

func workerPool(ctx context.Context, jobs <-chan Job, n int) <-chan Result {
results := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case j, ok := <-jobs:
if !ok { return }
results <- process(j)
}
}
}()
}
go func() { wg.Wait(); close(results) }()
return results
}

Pipeline — стадия → стадия через каналы:

nums := generate(ctx)
sq := square(ctx, nums)
out := filter(ctx, sq)
for v := range out { fmt.Println(v) }

Fan-out / fan-in:

// Fan-out: 1 канал → N горутин
// Fan-in: N каналов → 1
func fanIn(ctx context.Context, chans ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range chans {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
select {
case out <- v:
case <-ctx.Done(): return
}
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}

Semaphore через buffered channel:

sem := make(chan struct{}, 10) // max 10 concurrent
for _, task := range tasks {
sem <- struct{}{}
go func(t Task) {
defer func() { <-sem }()
run(t)
}(task)
}

Generator:

func gen(ctx context.Context, n int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; i < n; i++ {
select {
case out <- i:
case <-ctx.Done(): return
}
}
}()
return out
}
Окно терминала
go test -race ./...
go build -race ./...
go run -race main.go

Цена: ~5-10x CPU, ~2x memory, до 10x slower. В production не включаешь, в CI — обязательно.

  • Deadlock: 2 горутины ждут друг друга. Признаки: программа зависла, runtime: all goroutines are asleep - deadlock!.
  • Livelock: горутины двигаются, но не прогрессируют.
  • Starvation: одна горутина блокирует доступ другим (часто из-за приоритетов, RWMutex с постоянными читателями).

Как избегать:

  1. Всегда блокируй mutex’ы в одинаковом порядке.
  2. Используй context для отмены.
  3. Не держи lock на время сетевых вызовов.
  4. Buffered каналы — там, где это уместно, не везде.

Типичные сценарии:

  1. Send в канал без получателя:
ch := make(chan int) // нет буфера
go func() { ch <- 42 }() // вечно блокирует, если никто не читает
  1. Range по каналу, который никогда не закрывают.
  2. Горутина читает из канала, который потерял всех writer’ов.
  3. Тикеры/таймеры без Stop():
ticker := time.NewTicker(time.Second)
// забыли defer ticker.Stop() → leak

Как ловить:

  • runtime.NumGoroutine() в метриках.
  • pprof goroutine profile.
  • uber-go/goleak в тестах:
func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
  • Go 1.26 (2026) добавил встроенный goroutine leak profile через GC reachability analysis.

  • G (goroutine) — горутина с своим стеком (старт 2KB, растёт по необходимости).
  • M (machine) — OS-поток.
  • P (processor) — логический процессор, держит локальную очередь G. Число P = GOMAXPROCS (по умолчанию = логические ядра).

Работа:

  1. Когда горутина создаётся (go f()) — она кладётся в локальную run queue текущего P.
  2. M выполняет горутины из run queue своего P.
  3. Если локальная очередь пуста — M ворует половину очереди у другого P (work stealing).
  4. Если G делает блокирующий syscall — M отделяется, P берёт другой M, новые G идут к нему.
  5. Если G блокируется на канале/mutex — она паркуется (не занимает M).

Preemption: с Go 1.14 — асинхронный preemption через сигналы. Длинные циклы не блокируют scheduler.

Go использует non-moving, concurrent, tri-color, mark-and-sweep сборщик.

Цвета:

  • White — кандидат на удаление.
  • Gray — достижимый, ещё не просканирован.
  • Black — достижимый и просканирован.

Алгоритм:

  1. STW (stop-the-world) — старт, включение write barrier (~микросекунды).
  2. Concurrent mark — параллельно с программой.
  3. Mark termination (STW) — быстро.
  4. Concurrent sweep — освобождение белых.

Write barrier — функция, которая запускается при каждой записи указателя во время mark phase. Гарантирует, что не пропустим объект.

Параметры:

  • GOGC=100 (default) — GC запускается, когда heap вырос на 100% от размера live data в прошлом цикле.
    • GOGC=200 — реже GC, больше памяти.
    • GOGC=off — выключить.
  • GOMEMLIMIT=4GiB (Go 1.19+) — soft limit на heap. GC становится агрессивнее при приближении. Полезно в k8s/контейнерах с лимитом памяти.

В Go 1.25 экспериментально внедрён “Green Tea” — переработка алгоритма для уменьшения cache miss.

Подключение:

import _ "net/http/pprof"
go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()

Сбор:

Окно терминала
# CPU 30 секунд
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Heap
go tool pprof http://localhost:6060/debug/pprof/heap
# Горутины
go tool pprof http://localhost:6060/debug/pprof/goroutine
# Block (синхронизация)
go tool pprof http://localhost:6060/debug/pprof/block
# Mutex contention
go tool pprof http://localhost:6060/debug/pprof/mutex

В pprof UI:

  • top — топ функций.
  • list FuncName — построчно.
  • web — flame graph (нужен Graphviz).
  • traces — конкретные пути.

Для memory profile: inuse_space, inuse_objects, alloc_space, alloc_objects. Allocs дают понимание потерь.

func BenchmarkSum(b *testing.B) {
s := make([]int, 1000)
for i := range s { s[i] = i }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sum(s)
}
}

Запуск:

Окно терминала
go test -bench=. -benchmem -benchtime=10s -count=5 -cpu=1,2,4 > new.txt
benchstat old.txt new.txt

-benchmem показывает allocations. benchstat (golang.org/x/perf/cmd/benchstat) сравнивает прогоны статистически (Wilcoxon test).

import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ...

Или через pprof: curl 'http://localhost:6060/debug/pprof/trace?seconds=5' > trace.out.

Просмотр: go tool trace trace.out → браузер.

Видишь: горутины, syscalls, GC, network, scheduler latency.

  1. Преаллокация slice/map: make([]T, 0, n), make(map[K]V, n) — избежать ре-аллокаций.
  2. strings.Builder для конкатенации в цикле.
  3. sync.Pool для часто создаваемых объектов.
  4. Указатели vs value receiver: для маленьких структур (< 64 байт) — value. Для больших — pointer.
  5. Избегать interface{} в hot path — escape to heap.
  6. unsafe.String/unsafe.Slice для безаллокационного преобразования string ↔ []byte (с осторожностью).
  7. PGO (Profile-Guided Optimization) в Go 1.21+ — даёт 2-7% прироста бесплатно: go build -pgo=default.pgo.

Идиоматичный паттерн:

func TestParseInt(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{"valid positive", "123", 123, false},
{"negative", "-42", -42, false},
{"invalid", "abc", 0, true},
{"empty", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInt(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}

t.Run создаёт subtest, можно запускать выборочно: go test -run TestParseInt/valid_positive. Поддерживается t.Parallel() для параллельного запуска.

3 подхода:

  1. Ручные стабы (для маленьких интерфейсов) — лучший выбор по умолчанию.
  2. stretchr/testify/mock — assertions + mock в одном пакете.
  3. uber-go/mock (бывший gomock) + mockery — генерация mocks из интерфейсов.

Пример testify/mock:

type UserRepoMock struct { mock.Mock }
func (m *UserRepoMock) GetByID(ctx context.Context, id int) (*User, error) {
args := m.Called(ctx, id)
return args.Get(0).(*User), args.Error(1)
}
// Тест
repo := new(UserRepoMock)
repo.On("GetByID", mock.Anything, 1).Return(&User{ID: 1}, nil)
svc := NewService(repo)
u, err := svc.Fetch(ctx, 1)
require.NoError(t, err)
assert.Equal(t, 1, u.ID)
repo.AssertExpectations(t)

mockery генерирует mocks по интерфейсам — mockery --all --output mocks/.

func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/1", nil)
rec := httptest.NewRecorder()
h := NewHandler(repo)
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code)
}
}
// Mock внешнего HTTP-сервиса
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"ok": true}`))
}))
defer srv.Close()
client := NewClient(srv.URL)

Поднимать реальную БД для интеграционных тестов:

import "github.com/testcontainers/testcontainers-go/modules/postgres"
ctx := context.Background()
pg, _ := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("test"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
defer pg.Terminate(ctx)
dsn, _ := pg.ConnectionString(ctx, "sslmode=disable")
db, _ := sql.Open("pgx", dsn)
Окно терминала
go test -cover ./...
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out # html-отчёт
go tool cover -func=cover.out # сводка

Middle 1: цель 70-80% coverage на бизнес-логику. Не гонись за 100% — это бессмысленно.

func FuzzReverse(f *testing.F) {
testcases := []string{"Hello", "", "12345"}
for _, tc := range testcases {
f.Add(tc) // seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8: %q", rev)
}
})
}

Запуск: go test -fuzz=FuzzReverse -fuzztime=30s. Найденные failing inputs сохраняются в testdata/fuzz/.


db, err := sql.Open("pgx", dsn) // Open lazy, не открывает connection
if err != nil { return err }
defer db.Close()
// Обязательно: проверка живости
if err := db.PingContext(ctx); err != nil { return err }
// Настройки пула
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
db.SetConnMaxIdleTime(10 * time.Minute)
// Запрос с контекстом — ОБЯЗАТЕЛЬНО
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE active = $1", true)
if err != nil { return err }
defer rows.Close() // обязательно!
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil { return err }
// ...
}
return rows.Err() // не забудь!

Правила пула:

  • MaxOpenConns ≈ (CPU * 2) + 1 как стартовая точка, тюнить по нагрузке.
  • MaxIdleConns ≤ MaxOpenConns.
  • ConnMaxLifetime — особенно с балансировщиками (HAProxy/pgbouncer) и в k8s, чтобы пересоздавать stale connections.
  • Сумма MaxOpenConns всех инстансов сервиса ≤ лимит БД.

Prepared statements: db.Prepare() готовит на одном соединении, в пуле это сложно. Чаще передаёшь db.QueryContext(ctx, sql, args...) — pgx сам кэширует prepared statements при prefer_simple_protocol=false.

pgx — современный driver, рекомендуется для новых проектов:

  • Extended protocol по умолчанию с binary форматом данных.
  • Свой connection pool pgxpool — быстрее database/sql.
  • Поддержка PostgreSQL-специфичных типов (jsonb, hstore, arrays).
  • Активная поддержка.

lib/pq — больше не разрабатывается активно, использует Simple protocol → лишний parse-bind-execute на каждый параметризованный запрос.

Использование pgx:

// Через database/sql
import _ "github.com/jackc/pgx/v5/stdlib"
db, _ := sql.Open("pgx", dsn)
// Или нативный pgxpool (быстрее)
import "github.com/jackc/pgx/v5/pgxpool"
pool, _ := pgxpool.New(ctx, dsn)
rows, _ := pool.Query(ctx, "SELECT ...")
ИнструментКогда
database/sql + pgxПростые проекты, полный контроль.
sqlxЛёгкий wrapper над database/sql: Get, Select, StructScan. Удобно мапить результаты в структуры.
squirrelКогда нужно строить SQL динамически (фильтры по условиям). Сам не выполняет.
sqlcКомпилируешь SQL в Go-код на этапе сборки. Type-safe, минимум reflection. Топ-выбор в 2025.
GORMПолноценный ORM, миграции, hooks. Тяжёлый, reflection — медленнее. Хорош для прототипов/админок.
ent / bunАльтернативные ORM с code-gen или хорошей структурой.

В РФ-проектах часто: pgx + sqlc или pgx + sqlx + squirrel. GORM в production реже из-за производительности.

Пример sqlc (query.sql):

-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2;

После sqlc generate получаешь типизированные Go-функции.

tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil { return err }
defer tx.Rollback() // безопасно вызывать после Commit (no-op)
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", 100, fromID); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", 100, toID); err != nil {
return err
}
return tx.Commit()

Изоляции (PostgreSQL):

  • READ UNCOMMITTEDREAD COMMITTED (PG не имеет dirty read).
  • READ COMMITTEDdefault. Видишь только закоммиченное на момент запроса.
  • REPEATABLE READ — снапшот на момент начала транзакции. Защита от non-repeatable read, фантомы возможны в стандарте, но в PG MVCC ловит и их.
  • SERIALIZABLE — полная сериализация через SSI. Возможна ошибка serialization_failure — нужно ретраить.

Аномалии:

  • Dirty read — чтение незакоммиченного.
  • Non-repeatable read — повторное чтение даёт другие значения.
  • Phantom read — повторный range-запрос даёт новые строки.
  • Lost update — два обновления, одно потерялось.

Deadlocks: PostgreSQL детектирует через deadlock_timeout (по умолчанию 1s). В Go обычно ловишь по коду ошибки (40P01) и ретраишь.

goose (pressly/goose):

-- 20251001120000_create_users.sql
-- +goose Up
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
-- +goose Down
DROP TABLE users;
Окно терминала
goose -dir migrations postgres "$DSN" up
goose -dir migrations postgres "$DSN" down
goose -dir migrations postgres "$DSN" status

golang-migrate: похожая структура, два файла 001_create_users.up.sql / 001_create_users.down.sql.

Best practices:

  • Никогда не редактируй уже применённые миграции — только новые.
  • Up + Down всегда.
  • В production миграции — через CLI на CI/CD, не на старте приложения (иначе при 10 репликах будет конкуренция).
  • Если миграция упала в середине → “dirty” state, нужно ручное вмешательство (force version).

Слои (от внутреннего к внешнему):

  1. Entities (domain models) — структуры бизнес-объектов.
  2. Use Cases (services) — бизнес-логика.
  3. Interface Adapters (controllers, presenters, gateways).
  4. Frameworks & Drivers (HTTP, DB, gRPC).

Главное правило: зависимости направлены внутрь. Внутренние слои не знают о внешних. Между ними — интерфейсы.

Пример раскладки:

internal/
domain/ # сущности (User struct)
usecase/ # UserService с интерфейсами UserRepo, Notifier
repository/ # реализация UserRepo на pgx
delivery/
http/ # handlers
grpc/ # grpc-серверы
  • Entity — объект с идентичностью (User by ID).
  • Value Object — объект без идентичности, неизменяемый (Money, Email).
  • Aggregate — корень + сущности, изменяемые только через корень.
  • Repository — интерфейс доступа к persistence.
  • Domain Service — операция, не относящаяся к одной entity.
  • Bounded Context — границы между поддоменами.

Для Middle 1 — знать терминологию, понимать суть. Применение DDD в полном объёме — Senior.

Похоже на Clean Architecture. Ядро (domain + application) общается с внешним миром через ports (интерфейсы), которые реализуются adapters (БД, HTTP, очереди).

  • Driving (primary) port — приходящие запросы (HTTP handler вызывает use case).
  • Driven (secondary) port — исходящие (use case вызывает Repository, который реализован Postgres-адаптером).
// domain
type UserRepository interface {
GetByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, u *User) error
}
// infrastructure/postgres
type userRepo struct { db *pgxpool.Pool }
func (r *userRepo) GetByID(ctx context.Context, id int) (*User, error) { /* ... */ }
// usecase
type UserService struct { repo UserRepository }

Подходы:

  1. Ручной DI (compose root) — лучший выбор для большинства проектов:
func main() {
db := mustConnect()
repo := postgres.NewUserRepo(db)
svc := usecase.NewUserService(repo)
h := http.NewHandler(svc)
http.ListenAndServe(":8080", h)
}
  1. Google Wire — code-gen DI, compile-time:
wire.go
func InitializeApp() *App {
wire.Build(NewDB, NewUserRepo, NewUserService, NewHandler, NewApp)
return nil
}
  1. Uber fx — runtime DI с lifecycle:
app := fx.New(
fx.Provide(NewDB, NewUserRepo, NewUserService),
fx.Invoke(StartServer),
)

Совет: начинай с ручного DI. Wire — для больших статичных графов. Fx — когда нужен lifecycle и dynamic.

  • S — Single Responsibility: маленькие функции и интерфейсы.
  • O — Open/Closed: расширяй через новые типы, реализующие интерфейс.
  • L — Liskov Substitution: интерфейсы должны быть полноценно заменяемы.
  • I — Interface Segregation: главное в Go. Маленькие интерфейсы (io.Reader, io.Writer), интерфейс определяется на стороне потребителя.
  • D — Dependency Inversion: зависимости через интерфейсы → можно мокать в тестах.

Идиома Go: “Accept interfaces, return structs”. Принимай абстракции (Reader), возвращай конкретику (*MyStruct).

github.com/golang-standards/project-layout — НЕОФИЦИАЛЬНЫЙ стандарт, относись скептически. Минимум, который реально полезен:

  • cmd/myapp/main.go — entrypoint.
  • internal/ — пакеты, которые НЕ должны импортироваться извне (Go enforce’ит это).
  • pkg/ — публичные пакеты (опционально).
  • api/ — proto-файлы, OpenAPI specs.
  • migrations/ — SQL миграции.
  • deploy/ — k8s манифесты, helm чарты.
  • Makefile, Dockerfile, .golangci.yml, go.mod.

Принцип: простой проект — flat. Сложный — внутри internal/ группируй по домену, не по техническому слою.


mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}

Go 1.22 добавил pattern matching в http.ServeMux:

mux.HandleFunc("GET /users/{id}", userHandler)
id := r.PathValue("id")
РоутерОсобенности
chiИдиоматичен net/http, легковесный, отличная компоновка middleware, подходит для большинства задач. Хороший выбор по умолчанию.
ginСамый популярный (88k+ stars), быстрый, удобный API, BindJSON и т.д. Не на чистом net/http.
echoПохож на gin, баланс между фичами и простотой.
fiberНа fasthttp (не net/http), очень быстрый, но несовместим со стандартными middleware и http.Handler. Не рекомендуется без явных причин.
stdlib net/http (1.22+)После добавления path patterns достаточен для многих задач без сторонних роутеров.

Совет для middle: знай chi и net/http глубоко. gin — уметь читать существующий код. Fiber — знать, что он не на стандартном net/http и почему это часто проблема.

func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Цепочка
handler := loggingMiddleware(authMiddleware(mux))

В chi:

r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Get("/", indexHandler)

github.com/go-playground/validator/v10:

type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"gte=0,lte=130"`
}
var validate = validator.New()
if err := validate.Struct(req); err != nil { /* ... */ }

Два подхода:

  1. swag (swaggo/swag) — генерирует Swagger UI из комментариев. Code → docs.
  2. oapi-codegen (deepmap/oapi-codegen) — пишешь OpenAPI yaml, генерируешь handlers, models. Docs → code. Предпочтительный современный подход.

Структура proto:

syntax = "proto3";
package user.v1;
option go_package = "myapp/internal/gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User); // server stream
rpc UploadFile(stream Chunk) returns (UploadResult); // client stream
rpc Chat(stream Message) returns (stream Message); // bidi
}

Сервер:

type server struct { userv1.UnimplementedUserServiceServer }
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) { /* ... */ }
func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
userv1.RegisterUserServiceServer(s, &server{})
s.Serve(lis)
}

Клиент:

conn, _ := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := userv1.NewUserServiceClient(conn)
u, _ := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})

Знать: 4 типа RPC (unary, server stream, client stream, bidi), status.Errorf(codes.NotFound, ...), deadline propagation, interceptors.

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
// Закрыть DB, очереди, и т.д.

В k8s обязательно добавь паузу между SIGTERM и фактическим shutdown — балансер не успевает удалить pod из endpoints за 0 секунд.


Go 1.21+ стандартная библиотека log/slog:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("user signed in",
slog.Int("user_id", 42),
slog.String("ip", ip),
)
// {"time":"...","level":"INFO","msg":"user signed in","user_id":42,"ip":"..."}

Альтернативы:

  • zap (go.uber.org/zap) — самый быстрый, чуть больше allocations.
  • zerolog — fluent API, zero allocations в большинстве случаев.
  • logrus — устаревший, но ещё много кода.

Лучшие практики:

  • Используй structured (key-value), не printf.
  • Уровни: Debug/Info/Warn/Error.
  • Контекстные поля через slog.With() или logger = logger.With("request_id", id).
  • Не логируй секреты (PII, токены).
  • В production — JSON, в dev — text/console.
import "github.com/prometheus/client_golang/prometheus"
var (
httpRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total"},
[]string{"method", "endpoint", "status"},
)
httpDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"endpoint"},
)
)
func init() {
prometheus.MustRegister(httpRequests, httpDuration)
}
http.Handle("/metrics", promhttp.Handler())

4 типа метрик:

  • Counter — монотонно растущий (запросы, ошибки).
  • Gauge — может расти/падать (горутины, активные коннекшены).
  • Histogram — распределение (latency).
  • Summary — quantile, считается клиентом.

Правило RED: Rate, Errors, Duration — минимум для HTTP API. Правило USE: Utilization, Saturation, Errors — для ресурсов.

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
exp, _ := otlptracegrpc.New(ctx)
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp))
otel.SetTracerProvider(tp)
defer tp.Shutdown(ctx)
tr := otel.Tracer("myapp")
ctx, span := tr.Start(ctx, "GetUser")
defer span.End()
span.SetAttributes(attribute.Int("user.id", 42))

Концепции: Trace (всё путешествие запроса), Span (одна операция), Context propagation (через HTTP headers / gRPC metadata).

В каждом запросе — Trace ID + Request ID. Прокидывать через context и логи.

func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" { id = uuid.NewString() }
ctx := context.WithValue(r.Context(), reqIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

1.7
# --- Builder ---
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-trimpath \
-o /out/app ./cmd/app
# --- Runtime (distroless) ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

Финальный образ — ~5-15MB вместо ~700MB.

FROM scratch — ещё меньше (~3MB), но:

  • Нужно вручную копировать /etc/ssl/certs/ca-certificates.crt для HTTPS.
  • Нет shell, debug сложен.
  • distroless/static — лучший компромисс: есть TLS сертификаты, timezone, non-root юзер, но нет shell.

-ldflags="-s -w" убирает debug info из бинаря. -trimpath убирает абсолютные пути (reproducible builds). CGO_ENABLED=0 — статичный бинарь без glibc.

services:
app:
build: .
ports: ["8080:8080"]
environment:
DATABASE_URL: postgres://user:pass@db:5432/app?sslmode=disable
depends_on:
db: { condition: service_healthy }
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
healthcheck:
test: ["CMD", "pg_isready", "-U", "user"]
interval: 5s
volumes: [db-data:/var/lib/postgresql/data]
redis:
image: redis:7-alpine
volumes:
db-data:
  • Pod — наименьшая единица, 1+ контейнеров с общей сетью.
  • Deployment — управляет ReplicaSet, обеспечивает rolling update.
  • Service — стабильный endpoint для группы pod’ов (ClusterIP, NodePort, LoadBalancer).
  • Ingress — L7 роутинг.
  • ConfigMap / Secret — конфигурация / секреты.
  • HPA — горизонтальный автоскейлинг по метрикам.
  • Liveness / Readiness probes — k8s через HTTP/exec проверяет, жив ли pod / готов ли к трафику.

Минимальный manifest:

apiVersion: apps/v1
kind: Deployment
metadata: { name: myapp }
spec:
replicas: 3
selector: { matchLabels: { app: myapp } }
template:
metadata: { labels: { app: myapp } }
spec:
containers:
- name: myapp
image: myapp:1.0
ports: [{ containerPort: 8080 }]
livenessProbe: { httpGet: { path: /health, port: 8080 } }
readinessProbe: { httpGet: { path: /ready, port: 8080 } }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }

Не нужно уметь админить кластер. Нужно: писать манифесты для своего сервиса, читать логи через kubectl logs, понимать, почему pod в CrashLoopBackOff.


import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 50,
})
// Кэш
err := rdb.Set(ctx, "user:42", data, 10*time.Minute).Err()
val, err := rdb.Get(ctx, "user:42").Result()
if errors.Is(err, redis.Nil) { /* miss */ }
// Pub/Sub
sub := rdb.Subscribe(ctx, "channel")
for msg := range sub.Channel() { /* ... */ }
rdb.Publish(ctx, "channel", "hello")
// Distributed lock (упрощённо — лучше использовать redsync)
ok, _ := rdb.SetNX(ctx, "lock:job", "owner", 30*time.Second).Result()

Паттерны:

  • Cache-aside: код пытается прочитать из Redis, если miss → читает из БД, кладёт в Redis.
  • Write-through: пишешь в БД + Redis одновременно.
  • Distributed lock: SET NX EX + либо redsync (Redlock).

segmentio/kafka-go (proper Go-нативный) или confluentinc/confluent-kafka-go (на librdkafka, быстрее, требует C).

w := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: "events",
Balancer: &kafka.Hash{},
}
w.WriteMessages(ctx, kafka.Message{Key: []byte(userID), Value: payload})
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "events",
GroupID: "my-consumer-group",
})
for {
m, _ := r.FetchMessage(ctx)
process(m)
r.CommitMessages(ctx, m) // явный коммит после успешной обработки
}

Знать: topic, partition, offset, consumer group, semantics (at-least-once / at-most-once / exactly-once).

Лёгкая альтернатива Kafka для месседжинга:

nc, _ := nats.Connect(nats.DefaultURL)
defer nc.Close()
nc.Publish("subject", []byte("data"))
nc.Subscribe("subject", func(m *nats.Msg) { /* ... */ })

JetStream в NATS — persistence + replay (аналог Kafka, но проще).

rabbitmq/amqp091-go. AMQP-модель: exchanges → queues → consumers.

conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/")
ch, _ := conn.Channel()
ch.QueueDeclare("tasks", true, false, false, false, nil)
ch.Publish("", "tasks", false, false, amqp.Publishing{Body: data})

bradfitz/gomemcache — простой клиент. Для базового кэша, без persistence. В РФ-проектах сейчас в основном Redis вместо Memcached.

  • Простой кэш / pub-sub / locks → Redis.
  • Event streaming, аналитика, log pipelines → Kafka.
  • Лёгкая шина между микросервисами → NATS.
  • Сложный роутинг сообщений, work queues → RabbitMQ.
  • Background jobs → Asynq (на Redis) или Faktory.

11. Типичные вопросы на собеседовании Middle 1 Go

Заголовок раздела «11. Типичные вопросы на собеседовании Middle 1 Go»

Ниже 80 вопросов с подробными ответами. Они отсортированы по категориям и основаны на реальных собесах в Ozon, Avito, Яндекс, T-Bank, VK, Сбер в 2025-2026.

1. Что такое slice и чем отличается от array? Array — фиксированный размер, value type (копируется при присваивании). Slice — структура {*data, len, cap}, ссылается на underlying array. Передача slice по значению — копируется заголовок, но не данные.

2. Что произойдёт при append если cap не хватает? Создаётся новый underlying array (обычно в 2 раза больше для маленьких, ~1.25х для больших), данные копируются, slice указывает на новый массив. Старый — кандидат на GC.

3. Покажи код, который покажет неожиданное поведение append:

a := []int{1, 2, 3, 4, 5}
b := a[:3] // len=3 cap=5
c := append(b, 99) // НЕ создаёт новый массив, перезаписывает a[3]
fmt.Println(a) // [1 2 3 99 5]

4. Как удалить элемент из слайса по индексу? s = append(s[:i], s[i+1:]...) — теряет порядок не сохраняется, или для O(1) без сохранения порядка: s[i] = s[len(s)-1]; s = s[:len(s)-1]. С Go 1.21 есть slices.Delete.

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

  • s[i] — O(1).
  • append — амортизированно O(1), в худшем случае O(n).
  • Удаление из середины — O(n).
  • Поиск — O(n).

6. Что такое map изнутри? Hash table с buckets. Каждый bucket = 8 пар (key, value) + overflow pointer. При коллизии — добавляется в bucket или overflow. При load factor 6.5 — рост (incremental rehashing, эвакуация старых buckets в новые).

7. Почему iteration по map даёт разный порядок? Намеренно: Go рандомизирует стартовый bucket для предотвращения зависимостей на порядок.

8. Что произойдёт при concurrent чтении и записи в map? Runtime panic: fatal error: concurrent map read and map write (с Go 1.6+, есть детектор). Нужен sync.Mutex или sync.Map.

9. Когда использовать sync.Map?

  1. Ключ пишется один раз, читается много (cache). 2) Горутины работают с непересекающимися наборами ключей. В остальных случаях Mutex+map быстрее.

10. Чем строки отличаются от []byte? Строка immutable, {*data, len}. Конвертация string([]byte) и []byte(string) копирует данные. Для безаллокационной конвертации — unsafe.String/Slice.

11. strings.Builder vs += vs bytes.Buffer для конкатенации?

  • += в цикле — O(n²) и аллокации каждый раз.
  • bytes.Buffer — был стандартным, но конвертация в string копирует.
  • strings.Builderbest, no-copy String(). Можно Grow(n) для преаллокации.

12. Как работает range по строке? По rune (UTF-8), не по байтам. i — байтовый индекс, r — rune. Для байтов используй for i := 0; i < len(s); i++ { c := s[i] }.

Категория B: Указатели, методы, интерфейсы (13-25)

Заголовок раздела «Категория B: Указатели, методы, интерфейсы (13-25)»

13. Когда использовать value, а когда pointer receiver?

  • Pointer: мутирующий метод; большая структура; единообразие (если хоть один pointer — все pointer).
  • Value: маленький immutable тип; safer (нельзя случайно изменить).

14. Может ли interface быть nil? Да, но interface == nil ⇔ оба (tab, data) == nil. Если data == nil, но tab != nil — interface не nil:

var p *MyErr = nil
var err error = p
fmt.Println(err == nil) // false!

Это частый bug: возврат *MyErr где нужно error.

15. Что такое type assertion? Извлечение конкретного типа из interface: s := i.(string) — panic при неудаче. s, ok := i.(string) — safe.

16. Type switch — синтаксис?

switch v := i.(type) {
case int: ...
case string: ...
default: ...
}

17. Можно ли реализовать интерфейс в другом пакете? Да — Go использует структурную (duck) типизацию. Не нужно явно объявлять “implements”.

18. Что такое empty interface (any)? interface{} (или any с Go 1.18) — интерфейс без методов, удовлетворяется любым типом. Внутри — eface{*_type, data}.

19. Чем отличается embedded interface от embedded struct?

  • Embedded struct — поднимает поля и методы.
  • Embedded interface (в struct) — добавляет required методы (struct ДОЛЖЕН их реализовать или будет panic при вызове).
  • Embedded interface в interface — композиция требований.

20. Чему равен размер interface{} в памяти? 16 байт на 64-битной системе: 2 указателя.

21. Как работает динамический диспатч в Go? Через itab: вызов v.Method() → ищем в itab.fun[i] адрес конкретного метода → вызываем с data в качестве receiver.

22. Почему интерфейсы в Go “implicit”? Это позволяет добавлять интерфейсы к существующим типам без модификации исходного кода → decoupling.

23. Что такое stringer? Интерфейс Stringer { String() string } — кастомизирует вывод в fmt.Println, %v.

24. Как тестировать через интерфейс? Зависимости в коде — через интерфейсы. В тестах — mock-реализация.

25. Что вернёт reflect.TypeOf(nil)? nil. У nil нет конкретного типа. Похожая ловушка с typed nil — см. №14.

26. Чем горутина отличается от потока ОС?

  • Горутина: ~2KB стек на старте, управляется Go runtime, до миллионов.
  • OS thread: ~1MB+ стек, управляется ядром, тысячи.
  • M:N модель — много горутин на меньшем числе OS-потоков.

27. Что такое GOMAXPROCS? Число P в GMP-модели = число горутин, которые могут выполняться параллельно в данный момент. По умолчанию = число логических ядер.

28. Объясни буферизованный vs небуферизованный канал.

  • Небуферизованный (make(chan int)) — синхронный, send блокирует до recv.
  • Буферизованный (make(chan int, 10)) — асинхронный до заполнения буфера.

29. Что произойдёт при close() на канале?

  • После закрытия read возвращает zero value + ok=false.
  • Запись в закрытый канал → panic.
  • Закрытие закрытого → panic.
  • range завершается, когда канал закрыт и пуст.

30. Закрывает писатель или читатель канал? Писатель. Правило: “Don’t close a channel from the receiver side, and don’t close a channel if the channel has multiple concurrent senders”.

31. Как работает select? Случайно выбирает один из готовых cases. Если ни один не готов и есть default — выполняет его. Иначе блокируется на всех каналах одновременно.

32. Что такое sudog? Внутренняя структура runtime — представление горутины, ждущей на канале. В send/recv queue канала это связанный список sudog.

33. Напиши worker pool:

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 5; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range jobs {
results <- j * 2
}
}(w)
}
for j := 1; j <= 20; j++ { jobs <- j }
close(jobs)
go func() { wg.Wait(); close(results) }()
for r := range results { fmt.Println(r) }
}

34. Как ограничить число одновременных горутин? Buffered канал как семафор (см. раздел 2.6) или errgroup.SetLimit(n) (с Go 1.20).

35. Что такое goroutine leak? Приведи пример. Горутина зависла навсегда. Пример: горутина пишет в небуферизованный канал, для которого нет читателей (например, читатель завершился по timeout, но горутина не знает):

result := make(chan int)
go func() { result <- compute() }() // leak если никто не читает
select {
case r := <-result: fmt.Println(r)
case <-time.After(1*time.Second): return // горутина выше зависла
}

Решение: buffered (make(chan int, 1)) или передавать ctx и checked в горутине.

36. Как избежать race condition без mutex? Каналы (passing ownership), atomic, immutable data, sync.Once.

37. Что такое data race? Конкурентный доступ к памяти без синхронизации, где хоть один — write. Поведение UB.

38. Команда для проверки race conditions? go test -race, go run -race, go build -race.

39. select с одним каналом и time.After — паттерн? Timeout:

select {
case v := <-ch:
case <-time.After(5*time.Second):
return errors.New("timeout")
}

Caveat: time.After создаёт новый таймер при каждом вызове в цикле — leak. Лучше t := time.NewTimer(5*time.Second); defer t.Stop().

40. Найди баг:

for i := 0; i < 10; i++ {
go func() { fmt.Println(i) }()
}

До Go 1.22 — все горутины захватывают одну и ту же переменную i, выведут 10 десять раз. Исправление: go func(i int) { ... }(i). Go 1.22 изменил семантику — в каждой итерации новая i, проблема решилась автоматически.

41. Чем sync.WaitGroup отличается от семафора? WaitGroup — ждать завершения N задач (Add → Done → Wait). Семафор (buffered chan) — ограничить параллельность.

42. Как корректно завершить горутины при остановке сервиса? Через context.Context — каждая горутина слушает <-ctx.Done() и при отмене завершается. Главная горутина закрывает контекст и wg.Wait().

43. Объясни tri-color mark-and-sweep. Объекты делятся на white/gray/black. Roots → gray. Из gray сканируется, ссылки делаются gray, объект → black. Когда gray пуст — white удаляются.

44. Что такое write barrier? Hook, запускающийся при записи указателя во время GC mark — гарантирует, что GC не пропустит объект, переставленный в уже отсканированную часть графа.

45. STW (stop-the-world) в Go — длительность? В современном Go (1.20+) — ~10-100μs на типичных нагрузках. Двойное STW: на старте GC и mark termination.

46. Что такое GOGC? Параметр (default 100), задающий: GC запускается, когда heap вырос на GOGC% от размера live data после прошлого цикла.

47. Что такое GOMEMLIMIT? Soft limit на heap (Go 1.19+). GC становится агрессивнее при приближении к лимиту. Полезно в контейнерах с фиксированной памятью.

48. Escape analysis — что попадает на heap?

  • Указатель уходит за границы функции.
  • Конвертация в interface{}.
  • Замыкания, ловящие переменную по ссылке.
  • Slice/map переменного размера.
  • Слишком большие объекты.

49. Как посмотреть escape analysis? go build -gcflags="-m" main.go.

50. Чем отличается стек горутины от стека ОС? Стек горутины: растёт динамически (старт 2KB, max ~1GB), сегментированный (с 1.4 — copy-resize). OS-стек: фиксированный (обычно 1-8MB), однородный.

51. Что такое preemption? Прерывание выполнения горутины планировщиком. Go 1.14+ — асинхронная preemption через сигналы (раньше — только на функциях с stack check).

52. Как профилировать heap? go tool pprof http://.../debug/pprof/heaptop/list/web. Метрики: inuse_space (текущее), alloc_space (за всё время).

53. Когда полезен sync.Pool? Часто создаваемые короткоживущие объекты (buffers, encoders). Не для долгоживущих! GC может очистить pool.

54. Что такое work stealing? Когда P опустошает свою local run queue, M (привязанный к этому P) ворует половину очереди у другого P.

55. Что такое handoff в scheduler? Когда горутина уходит в блокирующий syscall, M отвязывается от P, новый M подхватывает P для обработки других G.

56. Что такое table-driven test? Тест с массивом случаев, итерация через t.Run. Стандартный идиоматичный паттерн.

57. testify vs стандартные асёрты? testify (require, assert) даёт богатые сообщения, require останавливает тест при ошибке. В std-библиотеке — t.Errorf, t.Fatalf.

58. Чем require отличается от assert в testify? require зовёт t.FailNow() (стоп теста). assertt.Fail() (продолжение).

59. Что такое t.Parallel()? Запуск этого subtest’а параллельно с другими subtest’ами того же родителя. Часто комбинируется с tt := tt (или с 1.22 — без него).

60. Как мокать time в тестах? Использовать абстракцию (Clock interface) или библиотеку (benbjohnson/clock, jonboulle/clockwork). Передавать в зависимости.

61. Чем integration отличается от unit-теста? Unit — изолирует под тестом. Integration — с реальными зависимостями (БД, HTTP). Запуск integration — отдельный шаг (например, //go:build integration).

62. Что такое coverage и какая цель? go test -cover — процент строк кода, выполненных тестами. Цель 70-80% на бизнес-логику. 100% не нужно.

63. Чем gRPC отличается от REST?

  • gRPC: HTTP/2, protobuf, кодогенерация, streaming, низкая латентность.
  • REST: HTTP/1.1, JSON, человекочитаемо, проще debug, кешируемо.
  • Часто внутри — gRPC, наружу — REST.

64. Когда middleware вызывается в chi? До основного хендлера (request flow) и в обратном порядке после (response flow). next.ServeHTTP(w, r) — переход к следующему.

65. Что делает http.Server.Shutdown()? Перестаёт принимать новые соединения, ждёт завершения текущих, закрывает idle. Получает context для timeout.

66. Зачем нужен ReadHeaderTimeout? Защита от slowloris-атак, где клиент медленно отправляет заголовки и держит соединение.

67. Что такое HTTP/2 multiplexing? Множество запросов в одном TCP-соединении параллельно (vs HTTP/1.1, где один запрос за раз или pipelining без перемешивания).

68. Что такое graceful shutdown? Прекратить принимать новые запросы, дать текущим завершиться, закрыть ресурсы (БД, очереди). Сигналы: SIGTERM (Kubernetes), SIGINT (Ctrl+C).

69. Как ограничить размер тела запроса? http.MaxBytesReader(w, r.Body, maxBytes).

70. Что такое connection pool? Кеш открытых соединений к БД. Зачем: TCP+TLS+auth дорого, переиспользовать.

71. Чем prepared statement отличается от обычного запроса? Сервер один раз парсит и планирует запрос, потом выполняешь с параметрами. Защита от SQL injection. В pgx — кеш auto.

72. ACID — расшифруй.

  • Atomicity — всё или ничего.
  • Consistency — переход между валидными состояниями.
  • Isolation — параллельные транзакции не мешают.
  • Durability — после commit данные сохранены.

73. Что такое индекс? Когда не нужно создавать? Структура (обычно B-tree) для ускорения поиска. Не нужно: на маленьких таблицах, на колонках с низкой кардинальностью, на write-heavy таблицах (индекс замедляет вставки).

74. SELECT N+1 — что это? Антипаттерн: 1 запрос на список + N запросов на детали. Решение: JOIN или batch.

75. Уровни изоляции в PostgreSQL?

  • READ COMMITTED (default) — нет dirty reads.
  • REPEATABLE READ — снапшот на старт транзакции.
  • SERIALIZABLE — полная изоляция через SSI.

Категория H: Архитектура, кодогенерация, прочее (76-80)

Заголовок раздела «Категория H: Архитектура, кодогенерация, прочее (76-80)»

76. Что такое Clean Architecture? Слои: entities → use cases → adapters → frameworks. Зависимости направлены внутрь, между слоями — интерфейсы.

77. Что такое idiomatic Go?

  • Маленькие интерфейсы.
  • Возврат ошибок (не паника).
  • Композиция > наследование.
  • gofmt-форматирование.
  • Краткие имена в коротком scope.
  • Accept interfaces, return structs.

78. Что такое “errors as values”? Ошибки — обычные значения, не исключения. Возвращаешь (T, error), проверяешь if err != nil. С 1.13+ — errors.Is, errors.As, обёртка через fmt.Errorf("%w", err).

79. Чем errors.Is отличается от ==? errors.Is разворачивает цепочку обёрнутых ошибок (через %w) и сравнивает на каждом уровне.

80. Что такое golangci-lint? Мета-линтер, агрегирует ~80 анализаторов (govet, staticcheck, errcheck, gosec и др.). Стандарт де-факто в Go-проектах.


Для Middle 1 нужно показать production-ready код: тесты, observability, Docker, миграции, чистая архитектура.

Стек: Go + chi + PostgreSQL (pgx + sqlc) + Redis + Prometheus + slog + Docker. Фичи:

  • POST /shorten — короткая ссылка (base62 от ID или nanoid).
  • GET /{code} — редирект.
  • Аналитика (счётчик кликов в Redis, batch flush в Postgres).
  • Rate limiter (token bucket).
  • Open API + Swagger.
  • Тесты (unit + integration через testcontainers).
  • Метрики, healthcheck, graceful shutdown.

Стек: Go + gRPC (или gorilla/websocket) + Redis pub/sub + PostgreSQL + Docker. Фичи:

  • Каналы (rooms), приватные сообщения.
  • История сообщений в PG.
  • Доставка через Redis pub/sub.
  • JWT auth.
  • gRPC bidirectional streaming.
  • Тесты на конкурентность.

Стек: Go + PostgreSQL + Redis (asynq) или Kafka + Prometheus. Фичи:

  • API для постановки задач.
  • Worker pool с graceful shutdown.
  • Retry с exponential backoff.
  • Dead letter queue.
  • Метрики throughput, latency, failure rate.
  • OpenTelemetry trace через всю цепочку.

Стек: Go (net/http/httputil) + Redis (rate limit) + OpenTelemetry. Фичи:

  • Маршрутизация (routes config из yaml/json).
  • Аутентификация (JWT).
  • Rate limiting (token bucket per user).
  • Circuit breaker.
  • Логи + трейсы.

Стек: Go + Kafka + ClickHouse + Prometheus + Grafana. Фичи:

  • HTTP endpoint собирает события.
  • Producer пишет в Kafka.
  • Consumer’ы batches пишут в ClickHouse.
  • Dashboard в Grafana.

Для каждого проекта:

  • README с диаграммой архитектуры.
  • Makefile (build, test, lint, run, migrate).
  • Docker + docker-compose.
  • .github/workflows/ci.yml (lint, test, build).
  • go.mod чистый.
  • Структура с internal/.

  1. “100 Go Mistakes and How to Avoid Them” — Teiva Harsanyi (2022, Manning). Перевод на русский есть. Топ-1 книга для Middle: 100 типовых ошибок, охватывает concurrency, тесты, типы, GC, контекст.
  2. “Concurrency in Go” — Katherine Cox-Buday (O’Reilly). Глубокое погружение в каналы, sync, паттерны.
  3. “The Go Programming Language” — Donovan & Kernighan (“K&R Go”). Классика.
  4. “Learn Go with Tests” — Chris James (бесплатно на GitHub) — TDD через стандартную библиотеку.
  5. “Efficient Go” — Bartłomiej Płotka (O’Reilly, 2022) — performance и профилирование на Go.
  6. “Distributed Services with Go” — Travis Jeffery — построение распределённой системы с нуля.
  • Yandex Practicum — Backend-разработчик на Go.
  • Otus — Golang Developer (basic + advanced).
  • Karpov.courses — Go-developer (с уклоном в data engineering).
  • Учебник Tinkoff Education — бесплатные конспекты по Go (education.tbank.ru).
  • balun.courses — глубокие курсы по Go, concurrency и собесам.
  • Канал “Анатолий Александрович” (YouTube) — разборы вопросов с собесов.
  • Антон Жуков (gophers.com.ua) — про concurrency и performance.
  • Канал “GoCloud” — про распределённые системы.
  • Telegram-каналы:
    • @golang_news — релизы, статьи.
    • @golang_interview — вопросы с собесов.
    • @gogolang — community.
    • @golangquiz — квизы для подготовки.
  • GolangConf (golangconf.ru) — записи докладов.
  • Dave Cheney (dave.cheney.net) — гуру Go, статьи про производительность и идиомы.
  • Bill Kennedy / Ardan Labs (ardanlabs.com/blog) — глубокие материалы по runtime.
  • Three Dots Labs (threedots.tech) — DDD, Clean Architecture в Go.
  • Boldly Go (boldlygo.tech) — обновления и патерны (2025).
  • VictoriaMetrics blog — производительность Go в реальных приложениях.
  • Allegro Tech Blog — про GC и production.
  • Eli Bendersky (eli.thegreenplace.net) — глубокая теория.
  • leetcode.com — алгоритмы.
  • gophercises.com — практические упражнения.
  • exercism.org/tracks/go — задачи с менторингом.

Реалистичный план для junior разработчика, который уже пишет на Go.

  • Неделя 1: Slice, map, string под капотом. Прочитать соответствующие главы из “100 Go Mistakes”. Решить 5 задач на манипуляции со слайсами.
  • Неделя 2: Интерфейсы (iface, eface, itab). Прочитать go-internals/chapter2_interfaces. Понять nil interface ловушку. Реализовать свой Stringer.
  • Неделя 3: Generics. Прочитать “When to use generics”. Сделать generic функции Map, Filter, Reduce, Stack[T].
  • Неделя 4: Reflect, unsafe (базы), escape analysis. Прогонять программы с -gcflags=-m.

Чекпоинт: ответить на 25 вопросов из категорий A и B этого roadmap.

  • Неделя 1: GMP-модель, goroutines, scheduler. Просмотреть видео “Go Scheduler Deep Dive”. Реализовать примеры с GOMAXPROCS.
  • Неделя 2: Channels (hchan), select внутри. Решить задачи: merge channels, fan-in, fan-out, pipeline.
  • Неделя 3: sync (Mutex, RWMutex, Once, Pool, Cond, Map), atomic, context. Прочитать главу про context в “100 Go Mistakes”.
  • Неделя 4: Race detector, goroutine leaks, паттерны (worker pool, semaphore). Использовать goleak в тестах.

Чекпоинт: написать с нуля worker pool с graceful shutdown, провалидировать через -race и goleak.

  • Неделя 1: pprof (cpu, heap, goroutine, block). Подключить к pet-проекту, найти 3 узких места.
  • Неделя 2: Benchmarks, benchstat. Написать бенчмарки на критические функции, сравнить варианты.
  • Неделя 3: GC, GOGC, GOMEMLIMIT, escape analysis. Прочитать “Go GC Guide”.
  • Неделя 4: Table-driven tests, testify, mockery, httptest, testcontainers. Покрыть pet-проект 80%+.

Чекпоинт: профилируешь свой сервис, оптимизируешь самую медленную функцию, измеряешь benchstat’ом.

  • Неделя 1: database/sql, pgx, connection pool. Настроить лимиты в реальном сервисе.
  • Неделя 2: Транзакции, изоляции, миграции (goose/golang-migrate).
  • Неделя 3: sqlc, sqlx, squirrel. Переписать ручные запросы на sqlc.
  • Неделя 4: slog, prometheus, OpenTelemetry. Добавить метрики и трейсы в pet-проект.

Чекпоинт: свой сервис показывает метрики в Grafana через Prometheus + trace в Jaeger.

  • Неделя 1: Clean Architecture, hexagonal. Перестроить pet-проект по слоям.
  • Неделя 2: net/http, chi, middleware, validation. Реализовать API с swagger.
  • Неделя 3: gRPC: proto, server, client, streaming, interceptors.
  • Неделя 4: Docker multi-stage, distroless, docker-compose, базы k8s (Pod, Deployment, Service, probes).

Чекпоинт: pet-проект в Docker с docker-compose, манифесты для k8s.

Месяц 6: Очереди + собесы + финальный pet-проект

Заголовок раздела «Месяц 6: Очереди + собесы + финальный pet-проект»
  • Неделя 1: Redis (кэш, pub/sub), Kafka producer/consumer.
  • Неделя 2: Завершение pet-проекта № 2 уровня middle (см. секцию 12).
  • Неделя 3: Mock-интервью. Прорешать 80 вопросов из секции 11. Системный дизайн: URL shortener, rate limiter, чат.
  • Неделя 4: Алгоритмы (топ-50 leetcode задач). Финализировать резюме и GitHub-портфолио.

Чекпоинт: прошёл 2-3 mock-интервью, готов идти на реальные собесы.

  • Каждую неделю — commits в pet-проект (минимум 3-5).
  • Каждые 2 недели — статья/перевод на Habr или Medium (не обязательно, но качает понимание).
  • Каждый месяц — митап или конференция (онлайн ок: GolangConf, GoCloud Conf).
  • Когда чувствуешь, что готов на 70% — подавай резюме. Реальные собесы — лучший учитель.