Goroutine leaks и дебаг concurrency
Зачем знать на Middle 1: Goroutine leak — это медленная смерть production-сервиса. Сначала вроде всё ок, потом OOM, потом сервис падает. Race condition — это бомба замедленного действия: тесты зелёные, prod падает раз в неделю с непонятной ошибкой. Middle-разработчик должен уметь: ловить leak через pprof, читать goroutine dump, использовать
-race, отлаживать concurrent код через delve/runtime/trace. Без этого — нельзя писать production-grade Go.
Содержание
Заголовок раздела «Содержание»- Базовая концепция (кратко)
- Под капотом: leak / deadlock / livelock / race
- Gotchas
- Производительность
- Когда использовать / альтернативы
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция (кратко)
Заголовок раздела «1. Базовая концепция (кратко)»Goroutine leak — горутина, которая никогда не завершится. Каждая занимает ~2KB stack + sched overhead. Накапливаясь, leak’и убивают сервис.
Deadlock — все горутины заблокированы, ждут друг друга. Runtime ловит “all goroutines asleep” и падает с fatal error.
Livelock — горутины активны, но не делают прогресс. Hard to detect.
Starvation — горутина никогда не получает ресурс (mutex, CPU).
Race condition — недетерминистичный результат из-за неконтролируемого порядка доступа к shared state. Ловится go test -race.
Инструменты:
runtime.NumGoroutine()— счётчик.pprof goroutine— stack trace всех.uber-go/goleak— leak detection в тестах.go test -race— race detector.delve— debugger с поддержкой горутин.runtime/trace— timeline исполнения.testing/synctest(Go 1.24+) — детерминистичные тесты.
2. Под капотом (детально)
Заголовок раздела «2. Под капотом (детально)»2.1. Что такое goroutine leak
Заголовок раздела «2.1. Что такое goroutine leak»Любая горутина, которая блокируется навсегда. Типовые причины:
A. Receive из канала, в который никто не отправит
Заголовок раздела «A. Receive из канала, в который никто не отправит»func leak1() { ch := make(chan int) go func() { v := <-ch // никто не отправит — горутина висит fmt.Println(v) }() // ch выйдет за scope, но горутина ссылается на него — GC не освободит.}B. Send в канал, из которого никто не читает (unbuffered)
Заголовок раздела «B. Send в канал, из которого никто не читает (unbuffered)»func leak2() { ch := make(chan int) go func() { ch <- 42 // блокировка }() // если caller не сделает <-ch, горутина висит}C. Forever loop без выхода
Заголовок раздела «C. Forever loop без выхода»func leak3() { go func() { for { doWork() time.Sleep(time.Second) } }() // нет способа остановить}D. Context.Done не проверяется
Заголовок раздела «D. Context.Done не проверяется»func leak4(ctx context.Context) { go func() { for { select { case data := <-dataChan: process(data) // нет case <-ctx.Done()! } } }()}При cancel ctx — ничего не происходит, горутина продолжает крутиться.
E. HTTP response body не closed
Заголовок раздела «E. HTTP response body не closed»resp, _ := http.Get(url)// забыли resp.Body.Close()io.ReadAll(resp.Body)Тут утечка не goroutine, но соединения. Под капотом keep-alive может держать reader-горутину.
F. time.Tick — старый паттерн
Заголовок раздела «F. time.Tick — старый паттерн»for range time.Tick(time.Second) { ... }time.Tick создаёт Ticker, который никогда не Stop. До Go 1.23 это был серьёзный leak. С Go 1.23 timer-GC лучше, но идиома всё равно плохая.
G. Goroutine ждёт WaitGroup, который никто не Done
Заголовок раздела «G. Goroutine ждёт WaitGroup, который никто не Done»var wg sync.WaitGroupwg.Add(1)go func() { wg.Wait() // если кто-то забыл Done — leak doNext()}()2.2. Как ловить leaks
Заголовок раздела «2.2. Как ловить leaks»Snapshot через runtime.NumGoroutine
Заголовок раздела «Snapshot через runtime.NumGoroutine»Самый простой способ:
func TestSomething(t *testing.T) { before := runtime.NumGoroutine() DoStuff() time.Sleep(100 * time.Millisecond) // дать выйти after := runtime.NumGoroutine() if after > before { t.Errorf("leak: %d → %d", before, after) }}⚠️ Минусы: flaky (timing-sensitive), не показывает где leak.
pprof goroutine profile
Заголовок раздела «pprof goroutine profile»В production:
import _ "net/http/pprof"
func init() { go http.ListenAndServe(":6060", nil)}Снять:
go tool pprof http://localhost:6060/debug/pprof/goroutineИли текстовый dump:
curl http://localhost:6060/debug/pprof/goroutine?debug=2Получишь stack trace каждой горутины:
goroutine 42 [chan receive, 5 minutes]:main.handler.func1(...) /app/main.go:123created by main.handler in goroutine 1 /app/main.go:120 +0x45
goroutine 87 [chan receive, 5 minutes]:main.handler.func1(...) /app/main.go:123created by main.handler in goroutine 1 /app/main.go:120 +0x4510 горутин в одном стеке “chan receive” 5 минут — это leak. Иди на main.go:123 — там receive из канала, который никто не пишет.
pprof с группировкой
Заголовок раздела «pprof с группировкой»go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutineВ браузере увидишь flame graph: какие функции создают больше всего горутин.
uber-go/goleak
Заголовок раздела «uber-go/goleak»import "go.uber.org/goleak"
func TestMain(m *testing.M) { goleak.VerifyTestMain(m)}
// Или per-test:func TestFoo(t *testing.T) { defer goleak.VerifyNone(t) // ...}После теста проверит, что нет лишних горутин. Если есть — fail с stack trace.
Continuous profiling
Заголовок раздела «Continuous profiling»В prod-системах:
- Pyroscope (grafana/pyroscope) — open source.
- DataDog Continuous Profiler.
- Google Cloud Profiler.
Снимают pprof раз в N сек, хранят историю. Можешь видеть рост горутин со временем.
2.3. Чтение goroutine dump
Заголовок раздела «2.3. Чтение goroutine dump»State горутины в stack trace:
| State | Значение |
|---|---|
runnable | готова к запуску, ждёт CPU |
running | выполняется |
syscall | в системном вызове |
chan send | ждёт send в канал |
chan receive | ждёт recv |
select | в select-block |
IO wait | ждёт сеть (netpoll) |
semacquire | ждёт mutex/cond |
sleep | time.Sleep |
wait | ждёт сигнал (различные) |
[chan receive, 5 minutes] — 5 минут в этом состоянии. Большие времена — потенциальный leak.
Группируй стеки: если 1000 горутин с одинаковым стеком в chan receive — это явный leak в этой точке.
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 \ | grep -A 2 "chan receive" \ | sort | uniq -c | sort -rn2.4. Best practices to avoid leaks
Заголовок раздела «2.4. Best practices to avoid leaks»1. Context propagation
Заголовок раздела «1. Context propagation»Каждая горутина — с context:
func work(ctx context.Context) { for { select { case <-ctx.Done(): return case v := <-ch: process(v) } }}2. Timeouts на каналы
Заголовок раздела «2. Timeouts на каналы»select {case v := <-ch: use(v)case <-time.After(5*time.Second): return ErrTimeout}⚠️ В горячем цикле — используй time.NewTimer + Reset, не time.After (см. файл 05).
3. defer cancel
Заголовок раздела «3. defer cancel»ctx, cancel := context.WithTimeout(parent, 5*time.Second)defer cancel() // ОБЯЗАТЕЛЬНО — иначе leak до timeoutДаже если успешно завершили — defer cancel освобождает таймер и goroutines контекста.
4. Bounded channels / worker pools
Заголовок раздела «4. Bounded channels / worker pools»Вместо:
for _, item := range millionItems { go work(item) // 1M горутин!}Используй пул:
sem := make(chan struct{}, 100)for _, item := range millionItems { sem <- struct{}{} go func(item Item) { defer func() { <-sem }() work(item) }(item)}5. Закрывай каналы детерминированно
Заголовок раздела «5. Закрывай каналы детерминированно»Закрывает sender. Если sender’ов несколько — координируй (WaitGroup + закрывающая горутина).
6. defer body.Close()
Заголовок раздела «6. defer body.Close()»resp, err := http.Get(url)if err != nil { return err }defer resp.Body.Close()io.ReadAll(resp.Body)Иначе keep-alive держит соединение в pool, может block-нуть.
2.5. Deadlock detection
Заголовок раздела «2.5. Deadlock detection»Compile-time
Заголовок раздела «Compile-time»Go не делает static analysis на deadlock. Никаких compile-time проверок.
Runtime: “all goroutines asleep”
Заголовок раздела «Runtime: “all goroutines asleep”»Если все горутины (включая main) заблокированы — runtime ловит и падает:
fatal error: all goroutines are asleep - deadlock!Это работает только для FULL deadlock. Partial — где часть горутин крутится, а часть deadlocked — runtime не ловит.
Пример full deadlock:
func main() { ch := make(chan int) <-ch // main ждёт, никого больше нет → fatal}Partial deadlock
Заголовок раздела «Partial deadlock»func main() { ch := make(chan int) go func() { for { // что-то делает, не использует ch time.Sleep(time.Second) } }() <-ch // main deadlock, но другая горутина живая — runtime молчит}Ловится через pprof goroutine dump или goleak в тестах.
Mutex deadlock
Заголовок раздела «Mutex deadlock»var mu1, mu2 sync.Mutex
go func() { mu1.Lock(); defer mu1.Unlock() mu2.Lock(); defer mu2.Unlock()}()
go func() { mu2.Lock(); defer mu2.Unlock() mu1.Lock(); defer mu1.Unlock()}()Classic ABBA deadlock. Решение: всегда захватывать mutex’ы в одном порядке.
2.6. Livelock
Заголовок раздела «2.6. Livelock»Горутины активны, но не делают прогресс. Пример:
// Два transactor пытаются дать друг другу:func transactor(other *Account) { for { mu1.Lock() if other.tryAcquire() { // ok return } mu1.Unlock() time.Sleep(time.Millisecond) // back off }}Если оба одновременно retry, могут вечно друг друга отталкивать. Решение: random backoff (jitter).
Livelock ловится только наблюдением: CPU 100%, прогресс 0%. pprof покажет hot loop.
2.7. Starvation
Заголовок раздела «2.7. Starvation»Когда горутина никогда не получает ресурс. Mutex starvation в Go 1.9+ предотвращена (см. файл 06). Но другие случаи:
// "богатый богаче": горутина с быстрой работой всегда возвращается первойfor { select { case <-fastCh: // быстрая обработка case <-slowCh: // не успевает, fastCh всегда срабатывает первым }}select random shuffle помогает, но если fastCh ВСЕГДА имеет данные — slowCh может starve.
Решение: приоритеты или раздельные горутины.
2.8. Race conditions
Заголовок раздела «2.8. Race conditions»Что такое race
Заголовок раздела «Что такое race»Два или более доступа к памяти, где хотя бы один — write, и нет synchronization между ними.
var counter intgo func() { counter++ }()go func() { counter++ }()// результат: 1 или 2, недетерминированноRace detector
Заголовок раздела «Race detector»go test -race, go build -race, go run -race.
Под капотом — ThreadSanitizer (TSan). Каждая загрузка/запись инструментируется. Хранит “shadow memory”:
- Для каждого байта — последний writer (goroutine + clock).
- При load/store проверяет — есть ли race с предыдущим.
Стоимость
Заголовок раздела «Стоимость»- CPU slowdown: 2-20x (зависит от concurrent access density).
- Memory: 5-10x (shadow memory).
- Goroutine creation: дороже.
Не для prod. Только dev/staging/CI.
Что находит
Заголовок раздела «Что находит»- Конкурентный write/write без mutex.
- Read/write без mutex.
- Race на initialization (init без guard).
Что НЕ находит
Заголовок раздела «Что НЕ находит»- Сам факт race может не сработать в данном run. Race detector только показывает реализованные races.
- Logical race (например, неправильный порядок lock — но без actual data race).
- Race в сторонних библиотеках без race-build.
Пример race с map
Заголовок раздела «Пример race с map»m := map[int]int{}go func() { m[1] = 1 }()go func() { m[2] = 2 }()go run -race:
==================WARNING: DATA RACEWrite at 0x00c0000a0030 by goroutine 7: runtime.mapassign_fast64() /usr/lib/go/src/runtime/map_fast64.go:92 +0x0 main.main.func1() /tmp/race.go:9 +0x80
Previous write at 0x00c0000a0030 by goroutine 6: runtime.mapassign_fast64() ...==================Готово, ты знаешь где race.
⚠️ Особенность: concurrent map access — это runtime fatal error, даже без race detector:
fatal error: concurrent map writes2.9. Дебаг concurrent кода
Заголовок раздела «2.9. Дебаг concurrent кода»delve (dlv)
Заголовок раздела «delve (dlv)»dlv debug main.go(dlv) break main.go:42(dlv) continue(dlv) goroutines # список всех горутин(dlv) goroutine 5 # переключиться на горутину 5(dlv) stack # stack trace(dlv) goroutine 5 frame 0 # переключиться на её фреймgoroutines показывает state и текущую строку. Полезно для “где зависла горутина”.
Print с goroutine ID
Заголовок раздела «Print с goroutine ID»В Go нет легального API для goroutine id (нарочно — discourage использования). Но можно через runtime.Stack:
func goid() int { var buf [64]byte n := runtime.Stack(buf[:], false) s := string(buf[:n]) s = strings.TrimPrefix(s, "goroutine ") s = s[:strings.IndexByte(s, ' ')] id, _ := strconv.Atoi(s) return id}Полезно для debug print:
log.Printf("[g%d] step", goid())⚠️ Только для debug. Никогда не используй в production logic — goroutine id может переиспользоваться.
runtime/trace
Заголовок раздела «runtime/trace»import "runtime/trace"
func main() { f, _ := os.Create("trace.out") defer f.Close() trace.Start(f) defer trace.Stop()
// ... работа ...}Анализ:
go tool trace trace.outОткроется браузер с timeline:
- View trace — timeline горутин.
- Goroutine analysis — статистика.
- Scheduler latency — задержки планирования.
Полезно для:
- Когда GC blocks goroutines.
- Где syscalls тормозят.
- Распределение нагрузки между P.
Trace для конкретного span
Заголовок раздела «Trace для конкретного span»В Go 1.21+:
ctx, task := trace.NewTask(ctx, "request")defer task.End()
trace.WithRegion(ctx, "db-query", func() { db.Query(...)})В trace tool увидишь иерархию: request → db-query.
2.10. Тестирование concurrent кода
Заголовок раздела «2.10. Тестирование concurrent кода»Не делать time.Sleep — flaky tests
Заголовок раздела «Не делать time.Sleep — flaky tests»// ПЛОХОgo startWorker()time.Sleep(100*time.Millisecond) // надежд?// проверкаSleep — racing timer с реальной системой. CI ляжет, локально ок. Используй:
sync.WaitGroup + assert.Eventually
Заголовок раздела «sync.WaitGroup + assert.Eventually»var wg sync.WaitGroupwg.Add(1)go func() { defer wg.Done() work()}()wg.Wait() // гарантия что закончилосьtestify/assert.Eventually:
assert.Eventually(t, func() bool { return stats.Processed() == 100}, 5*time.Second, 10*time.Millisecond)Опрос с интервалом, до timeout. Менее flaky чем sleep.
testify suite
Заголовок раздела «testify suite»type WorkerSuite struct { suite.Suite pool *Pool}
func (s *WorkerSuite) SetupTest() { s.pool = NewPool(s.T().Context())}
func (s *WorkerSuite) TearDownTest() { s.pool.Stop()}
func (s *WorkerSuite) TestSubmit() { s.NoError(s.pool.Submit(work))}
func TestWorkerSuite(t *testing.T) { suite.Run(t, &WorkerSuite{})}testing/synctest (Go 1.24+, экспериментально)
Заголовок раздела «testing/synctest (Go 1.24+, экспериментально)»Новый пакет для детерминистичных тестов concurrency. Создаёт “bubble” с виртуальным временем, синхронизирует горутины.
import "testing/synctest"
func TestTimer(t *testing.T) { synctest.Run(func() { start := time.Now() time.Sleep(time.Hour) if time.Since(start) != time.Hour { t.Fail() } })}Внутри synctest.Run все time.Sleep, time.After, time.NewTimer работают с фейковым временем. Тест выполняется мгновенно, но логически “проходит” час.
Доступно в Go 1.24+. Раньше использовали clockwork/quartz библиотеки.
Testing race detector
Заголовок раздела «Testing race detector»go test -race ./...В CI обязательно. Если race detector что-то нашёл — баг.
Multiple GOMAXPROCS
Заголовок раздела «Multiple GOMAXPROCS»GOMAXPROCS=1 go test ./...GOMAXPROCS=8 go test ./...Некоторые race видны только при разной concurrency.
Stress tests
Заголовок раздела «Stress tests»func TestStress(t *testing.T) { if testing.Short() { t.Skip() } for i := 0; i < 10000; i++ { runConcurrentScenario() }}В CI запускать с -count=10 или -count=100 для flushing race-ов.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1. ⚠️ runtime.NumGoroutine считает все, включая system
Заголовок раздела «3.1. ⚠️ runtime.NumGoroutine считает все, включая system»fmt.Println(runtime.NumGoroutine()) // 5 при старте программы!Системные горутины: GC, finalizer, scavenger, sysmon. Snapshot — не точно user count.
3.2. ⚠️ Goroutine id нельзя использовать в логике
Заголовок раздела «3.2. ⚠️ Goroutine id нельзя использовать в логике»id := goid() // меняется между restart, переиспользуетсяЭто implementation detail рантайма. Если нужен logical id — генерируй сам и пробрасывай через context.
3.3. ⚠️ pprof goroutine?debug=2 stack может быть огромным
Заголовок раздела «3.3. ⚠️ pprof goroutine?debug=2 stack может быть огромным»Под нагрузкой 100k goroutines — dump на десятки MB. Использовать debug=1 для compact формата (агрегация по stack).
3.4. ⚠️ Race detector не ловит вне -race builds
Заголовок раздела «3.4. ⚠️ Race detector не ловит вне -race builds»Сторонние библиотеки могут содержать race-ы, видные только при сборке всей программы с -race. Поэтому CI должен запускать race tests на полном коде.
3.5. ⚠️ -race не для prod (10x slowdown)
Заголовок раздела «3.5. ⚠️ -race не для prod (10x slowdown)»Иногда команды думают: “включим в production для безопасности”. Не делай — 10x slowdown, 10x memory. Используй в dev/staging.
3.6. ⚠️ “all goroutines asleep” не отловит partial deadlock
Заголовок раздела «3.6. ⚠️ “all goroutines asleep” не отловит partial deadlock»go func() { for {} }() // живаяch := make(chan int)<-ch // deadlock, но runtime молчитИспользуй goleak или pprof.
3.7. ⚠️ time.Sleep в тестах — flaky
Заголовок раздела «3.7. ⚠️ time.Sleep в тестах — flaky»CI окружения тормознее локалки → 100ms sleep может не хватить. Используй WaitGroup / Eventually.
3.8. ⚠️ Wait() блокирует если Add(N) > N×Done
Заголовок раздела «3.8. ⚠️ Wait() блокирует если Add(N) > N×Done»Забыл Done в одной ветке (например, ошибка раньше) — Wait висит.
Решение: всегда defer wg.Done() сразу после wg.Add(1) (или внутри goroutine).
3.9. ⚠️ goleak ловит ложно-позитивные
Заголовок раздела «3.9. ⚠️ goleak ловит ложно-позитивные»Системные горутины (например, runtime test framework) тоже считаются. Используй goleak.IgnoreCurrent() для baseline.
3.10. ⚠️ pprof goroutine не показывает stuck в C-коде
Заголовок раздела «3.10. ⚠️ pprof goroutine не показывает stuck в C-коде»CGo goroutine в syscall — pprof покажет state syscall, но stack из C-кода не виден. Профилируй через perf/dtrace.
3.11. ⚠️ defer cancel забывают
Заголовок раздела «3.11. ⚠️ defer cancel забывают»ctx, _ := context.WithCancel(parent)// ctx используется, но cancel никто не вызовет — leak до GC родителяvet “lostcancel” ловит это. Не игнорируй warning.
3.12. ⚠️ Map concurrent write — фатально
Заголовок раздела «3.12. ⚠️ Map concurrent write — фатально»Не race detector — runtime fatal error. Падает без recover.
fatal error: concurrent map writesЗащита: Mutex или sync.Map.
4. Производительность
Заголовок раздела «4. Производительность»4.1. Goroutine cost
Заголовок раздела «4.1. Goroutine cost»- Stack initial: 2 KB (grows by doubling до 1 GB).
- Sched overhead: ~100 ns per gocall.
- Context switch: ~200-500 ns.
1M goroutines = ~2 GB stack минимум.
4.2. NumGoroutine() стоимость
Заголовок раздела «4.2. NumGoroutine() стоимость»runtime.NumGoroutine() — атомарный счётчик, ~10 ns. Безопасно вызывать часто (для метрики).
4.3. pprof overhead
Заголовок раздела «4.3. pprof overhead»runtime.Stack для goroutine dump — берёт stopTheWorld короткое время. Под нагрузкой 100k goroutines — 50-200 ms блокировки всей программы. Не делай в hot path.
4.4. Race detector
Заголовок раздела «4.4. Race detector»| Метрика | Slowdown |
|---|---|
| CPU time | 5-15x |
| Memory | 5-10x |
| Goroutine create | 2-3x |
Запускай unit-tests с -race, не bench.
4.5. runtime/trace overhead
Заголовок раздела «4.5. runtime/trace overhead»trace.Start записывает events. ~3-5% overhead. Файл растёт быстро (10-100 MB/s под нагрузкой). Использовать для коротких профилей (10-30 сек).
5. Когда использовать / альтернативы
Заголовок раздела «5. Когда использовать / альтернативы»Detection method matrix
Заголовок раздела «Detection method matrix»| Что детектим | Инструмент |
|---|---|
| Goroutine leak (dev) | goleak в тестах |
| Goroutine leak (prod) | pprof goroutine + alert на NumGoroutine |
| Race | go test -race |
| Deadlock полный | runtime автоматически (fatal) |
| Deadlock partial | pprof + manual review |
| Livelock | pprof CPU + manual review |
| Slow concurrent code | runtime/trace |
| Mutex contention | go test -mutexprofile |
| Block (channels, semaphore) | go test -blockprofile |
В каких case’ах какой подход
Заголовок раздела «В каких case’ах какой подход»Лёгкий случай (один сервис, маленький trafic):
- Race в CI.
- pprof endpoint в prod.
- Alert на NumGoroutine > 10k.
Серьёзный случай (high-load backend):
- Continuous profiling (Pyroscope).
- goleak в integration tests.
- runtime/trace для деградаций.
- Distributed tracing (для multi-service).
Прод инциденты:
- Скачай pprof goroutine dump.
- Группируй по stack.
- Найди подозрительный stack (long state).
6. Вопросы на собесе
Заголовок раздела «6. Вопросы на собесе»1. Что такое goroutine leak? Горутина, которая никогда не завершится, удерживая память и ресурсы. Типичные причины: receive из канала без sender, forever loop без cancel, context.Done не проверяется.
2. Как поймать leak в проде?
pprof goroutine profile: curl /debug/pprof/goroutine?debug=2. Stack trace всех горутин, ищи длительные state (chan receive, semacquire).
3. Чем debug=1 отличается от debug=2?
debug=1 — агрегированный (sample stacks с count). debug=2 — полный dump каждой горутины с stack trace.
4. Что такое goleak?
Библиотека uber-go/goleak. В тестах вызывает goleak.VerifyNone(t) — проверяет что после теста нет утёкших горутин (кроме baseline).
5. Как ловить deadlock? Полный — runtime сам падает с “all goroutines are asleep”. Частичный — pprof + анализ stack trace.
6. Что такое livelock? Горутины активны (CPU занят), но прогресса нет (например, бесконечный retry без backoff). Pprof CPU profile покажет hot loop.
7. Когда возникает starvation?
Когда одна горутина никогда не получает ресурс. В Go 1.9+ mutex starvation предотвращена (starvation mode). Но select может starve если один канал всегда быстрее.
8. Race condition vs data race? Data race — конкретное явление: 2+ доступа к памяти, хоть один — write, без sync. Race condition — более общее: результат зависит от тайминга.
9. Как работает race detector? ThreadSanitizer (TSan). Инструментирует load/store. Shadow memory хранит last writer (goroutine + clock). При каждом доступе проверяет hb-relation с предыдущим.
10. Стоимость race detector? CPU 5-15x, memory 5-10x. Только для dev/staging/CI. НЕ для prod.
11. Что ловит и не ловит race detector? Ловит: data races, видные в данном run. НЕ ловит: logical race без data race, races не сработавшие в текущем run, races в кодах без -race флага.
12. Как go test -race запустить?
go test -race ./.... В CI обязательно. Можно стрессить: go test -race -count=100 ./....
13. Как избежать leak в context’е?
Всегда defer cancel() после context.WithCancel/Timeout. Все горутины проверяют ctx.Done() в select.
14. Что делает go vet для concurrency?
copylocks— копирование Mutex/WaitGroup.lostcancel— потерянный cancel из WithCancel.loopclosure— capture переменной цикла в горутине (старая семантика до Go 1.22).
15. Зачем defer cancel(), если ctx уже expired?
cancel освобождает ресурсы контекста (таймеры, дочерние goroutines). Без cancel — leak до timeout родительского ctx.
16. State горутины в dump: что такое semacquire? Горутина ждёт semaphore — внутри Mutex.Lock, sync.Cond.Wait, или channel parking.
17. State IO wait что значит?
Горутина в netpoll: ждёт ready на сетевом socket. Не блокирует thread.
18. Что такое stack trace created by?
Где была создана горутина. goroutine X [chan receive]: ... created by Y at file:line — горутина создана функцией Y.
19. Партиал deadlock — как ловить?
pprof goroutine + visual inspection. Если несколько горутин в chan receive или semacquire долго — подозрение.
20. Что делает runtime.NumGoroutine?
Возвращает количество active goroutines. Включает системные (GC, finalizer и т.д.). Атомарный счётчик, ~10ns.
21. Можно ли получить goroutine id?
Через runtime.Stack — parse string. Но это не публичное API, нельзя в логике. Только для debug log.
22. testing/synctest зачем? Go 1.24+. Детерминистичные тесты concurrency: фейковое время, синхронизированные горутины. Заменяет sleep-based testing.
23. Что показывает go tool trace?
Timeline всех горутин, GC pauses, syscalls, lock contention, goroutine state changes. Для очень глубокого анализа.
24. block profile vs mutex profile?
- Block — ВСЕ блокировки (channel, mutex, semacquire).
- Mutex — только sync.Mutex contention.
25. Когда time.Tick опасен? Не освобождается после выхода из range. До Go 1.23 — leak. Используй NewTicker + defer Stop().
26. Закрытие resp.Body — почему важно? HTTP keep-alive держит соединение в pool. Без Close — connection (а с ней read goroutine) остаётся открытой.
27. testify Eventually vs sleep? Eventually — polling с интервалом, до timeout. Менее flaky чем sleep (не недосыпает на медленном CI).
28. concurrent map writes — что случится?
Runtime panic: fatal error: concurrent map writes. Без recover. Защита: Mutex или sync.Map.
29. Как stop GoRoutine “снаружи”? Нельзя силой. Нужно — сама горутина должна проверять сигнал (context.Done, канал done). Goroutine это не thread с интерфейсом kill.
30. Что такое stop-the-world (STW)?
Все горутины приостанавливаются. Бывает при GC marking start/finish, при runtime.Stack для всех горутин (all=true), при pprof goroutine?debug=2.
7. Practice
Заголовок раздела «7. Practice»Задача 1. Воспроизвести и пофиксить leak
Заголовок раздела «Задача 1. Воспроизвести и пофиксить leak»// Сломанная версияfunc brokenFetch(url string) string { ch := make(chan string) go func() { resp, _ := http.Get(url) body, _ := io.ReadAll(resp.Body) ch <- string(body) }() select { case s := <-ch: return s case <-time.After(1*time.Second): return "" // leak: горутина продолжает работу, ch unbuffered }}Fix:
func fixedFetch(ctx context.Context, url string) string { ch := make(chan string, 1) // буферизованный — sender не leak go func() { req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := http.DefaultClient.Do(req) if err != nil { ch <- "" return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) ch <- string(body) }() select { case s := <-ch: return s case <-ctx.Done(): return "" }}Изменения:
chan string, 1— sender не блокируется.NewRequestWithContext— отмена request при cancel.defer resp.Body.Close().
Задача 2. Test с goleak
Заголовок раздела «Задача 2. Test с goleak»func TestPipeline(t *testing.T) { defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
out := pipeline(ctx, []int{1, 2, 3}) for range out { }}Если в pipeline что-то leak’нёт — тест упадёт с stack trace.
Задача 3. Race condition exposure
Заголовок раздела «Задача 3. Race condition exposure»// Racetype Counter struct { n int}
func (c *Counter) Inc() { c.n++ }func (c *Counter) Get() int { return c.n }
func TestCounterRace(t *testing.T) { c := &Counter{} var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() if c.Get() != 100 { t.Fail() // не всегда: может быть 90, 95, ... }}go test -race — найдёт.
Fix:
type Counter struct { n atomic.Int64}
func (c *Counter) Inc() { c.n.Add(1) }func (c *Counter) Get() int64 { return c.n.Load() }Задача 4. Mutex deadlock ABBA
Заголовок раздела «Задача 4. Mutex deadlock ABBA»func deadlockABBA() { var muA, muB sync.Mutex done := make(chan struct{}, 2)
go func() { muA.Lock() time.Sleep(10*time.Millisecond) muB.Lock() muB.Unlock() muA.Unlock() done <- struct{}{} }()
go func() { muB.Lock() time.Sleep(10*time.Millisecond) muA.Lock() muA.Unlock() muB.Unlock() done <- struct{}{} }()
<-done; <-done // зависнет}Запусти — runtime fatal “all goroutines are asleep” (если main тоже завис на recv).
Fix: всегда захватывать в одном порядке (A, потом B).
Задача 5. Анализ pprof goroutine dump
Заголовок раздела «Задача 5. Анализ pprof goroutine dump»Установи проект с leak, запусти, открой:
http://localhost:6060/debug/pprof/goroutine?debug=2Найди:
- Сколько горутин всего.
- Группы по одинаковому stack.
- Top по state длительности.
Задача 6. testing.Eventually
Заголовок раздела «Задача 6. testing.Eventually»func TestBackgroundProcess(t *testing.T) { proc := startProcessor() defer proc.Stop()
proc.Submit(work{})
require.Eventually(t, func() bool { return proc.Processed() >= 1 }, time.Second, 10*time.Millisecond, "should process within 1s")}Лучше чем time.Sleep(time.Second).
Задача 7. runtime/trace
Заголовок раздела «Задача 7. runtime/trace»func main() { f, _ := os.Create("trace.out") defer f.Close() if err := trace.Start(f); err != nil { panic(err) } defer trace.Stop()
runWorkload()}Запусти, открой:
go tool trace trace.outИзучи:
- View trace: горутины по timeline.
- Goroutine analysis: top по runtime.
- Synchronization blocking profile.
Найди bottleneck. Часто это lock contention или GC.
Задача 8. testing/synctest (Go 1.24+)
Заголовок раздела «Задача 8. testing/synctest (Go 1.24+)»func TestRetry(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
err := retryWithBackoff(ctx, func() error { return errors.New("fail") }) if err == nil { t.Fatal("expected error") } })}time.Sleep внутри retry будет fake — тест выполнится мгновенно, но retry “сделает” свои паузы.
8. Источники
Заголовок раздела «8. Источники»- uber-go/goleak — https://github.com/uber-go/goleak.
- Russ Cox — “Data Race Detector” — https://go.dev/blog/race-detector.
- Go Diagnostics — https://go.dev/doc/diagnostics (pprof, trace, race).
- Profiling Go programs — https://go.dev/blog/pprof.
- runtime/trace docs — https://pkg.go.dev/runtime/trace.
- delve documentation — https://github.com/go-delve/delve/tree/master/Documentation.
- testing/synctest proposal — https://go.dev/issue/67434.
- Common Go Concurrency Pitfalls — https://go.dev/wiki/CommonMistakes.
- The Go Memory Model — https://go.dev/ref/mem.
- dave.cheney.net — “Never start a goroutine without knowing how it will stop” — https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop.