Утечки goroutine и ресурсов
Утечка — работа или ресурс, которые больше не достижимы по бизнес-смыслу, но остаются живыми: goroutine ждёт channel/I/O/lock, Rows удерживает connection, HTTP response body не возвращает connection в pool, timer ждёт срок. GC не решает это автоматически: goroutine и внешние дескрипторы имеют собственный lifecycle.
Зачем это на интервью
Рост runtime.NumGoroutine, исчерпание pool или FD, зависающий shutdown и tail latency часто имеют одну причину — неявное ownership. Нужен воспроизводимый способ найти блокировку, назвать owner каждого go, channel, Close и Cancel, затем подтвердить исправление тестом и профилем.
Минимум для E4
- Для каждого
goназвать событие выхода, owner запуска и способ ожидания (WaitGroup,errgroup, result channel). - В
selectпри potentially blocking send/receive наблюдатьctx.Done(). - Закрывать
resp.Body,sql.Rows, files иTicker; останавливать timers когда это требуется. - Ограничивать очередь и concurrency, а не создавать goroutine на каждое входящее событие без лимита.
- Диагностировать через goroutine profile, block/mutex profile, trace и метрики открытых ресурсов.
Углубление для E5/Senior
E5 проектирует structured concurrency: родитель владеет детьми и ждёт их outcome; producer владеет close output channel; cancellation прекращает admission, а Wait подтверждает termination. Он отличает normal long-lived goroutine (server accept loop) от leak по steady-state baseline, задаёт limits и оценивает FD/pool budget под peak traffic.
Ключевые понятия
| Признак | Частая причина | Проверка/исправление |
|---|---|---|
| Растёт goroutine count | blocked send, lost receiver, infinite retry | /debug/pprof/goroutine?debug=2, cancellation branch |
| Pool исчерпан | не закрыли rows/body, transaction долго живёт | defer Close, pool stats, bounded transaction |
| Растут FD | file/socket response не закрыт | lsof, process FD metric, owner Close |
| CPU после cancel | busy loop/default select | block until event, add backoff/exit |
| Shutdown висит | worker не observes stop | root ctx + Wait with deadline |
Bounded worker pool
package workers
import (
"context"
"sync"
)
func Process(ctx context.Context, jobs <-chan int, n int, handle func(context.Context, int) error) error {
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 job, ok := <-jobs:
if !ok {
return
}
_ = handle(ctx, job) // Production code должен определить policy ошибки.
}
}
}()
}
wg.Wait()
return ctx.Err()
}Imports: context, sync. Owner jobs закрывает канал после завершения producer; workers никогда не закрывают input. Эта версия ждёт workers синхронно и не возвращает рабочие ошибки: для fail-fast используйте errgroup.WithContext, но всё равно убедитесь, что producer может разблокироваться при cancel.
Rows и response body
func listNames(ctx context.Context, db *sql.DB) ([]string, error) {
rows, err := db.QueryContext(ctx, "SELECT name FROM users")
if err != nil { return nil, err }
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil { return nil, err }
names = append(names, name)
}
return names, rows.Err()
}Imports: context, database/sql. rows.Close важен и после partial iteration; rows.Err ловит ошибки доставки после успешного QueryContext. Для HTTP всегда defer resp.Body.Close() после non-nil response и читайте body ровно так, как требует reuse policy transport.
Типовые вопросы
- Почему goroutine не исчезает после return caller?
- Goroutine независима; она живёт, пока сама не вернётся. Caller обязан передать termination signal и при необходимости дождаться её.
- Кто закрывает channel?
- Обычно единственный sender/producer, который знает, что новых значений не будет. Receiver закрывать не должен.
- Нужен ли
defer ticker.Stop()?- Да для ticker с ограниченным lifecycle, чтобы освободить связанные runtime resources; канал ticker не закрывается.
- Как отличить leak от busy service?
- Сравнить steady-state baseline по нагрузке, снять goroutine stack и связать рост с request/queue/FD метриками.
- Почему
WaitGroupне отменяет work?- Он только считает завершения; stop signal передаёт context/channel, затем
Waitподтверждает exit.
- Он только считает завершения; stop signal передаёт context/channel, затем
- Что делает
resp.Body.Close?- Освобождает body и позволяет transport корректно управлять connection; отсутствие close ведёт к исчерпанию ресурсов.
Практика
- Создайте тест на producer, который блокируется при остановленном consumer.
- Критерии приёмки: cancel разблокирует send;
WaitGroupзавершает workers; повтор 100 раз не показывает роста goroutine;go test -raceпроходит.
- Критерии приёмки: cancel разблокирует send;
- Найдите искусственную leak в HTTP/SQL коде.
- Критерии приёмки: приложен goroutine/FD/pool symptom до исправления; исправление закрывает body/rows на всех return paths; регрессионный тест проверяет cancel.
Частые ошибки и ловушки
go func(){ ch <- value }()как способ не блокировать sender: leak просто становится скрытой.recoverвокруг send в закрытый channel вместо явного ownership.- Держать DB transaction или mutex во время сетевого RPC.
- Считать
runtime.GC()средством лечения FD/goroutine leak.
Связанные темы
Context и жизненный цикл · Propagation · Shutdown сервиса · Наблюдаемость
Источники
- Go blog: Pipelines and cancellation — cancellation and goroutine leak patterns.
- Go
runtime/pprof— profiles goroutine/block/mutex. - Go
database/sql.Rows.Close— lifecycle database rows. - Go
net/http.Response.Body— необходимость закрыть response body.