TGViewer
Библиотека Go-разработчика | Golang Библиотека Go-разработчика | Golang @goproglib · 24.1K subscribers
Post #7314 2.75K
🧑‍💻 Утечка горутин из-за каналов, которые никто не закрывает

Go делает конкурентность простой. Горутины дешёвые, каналы встроены в язык, а воркер-пул собирается меньше чем за 30 строк. Но из-за этой простоты в код проникают баги. Программа компилируется, тесты проходят, а ошибка всплывает только под нагрузкой или после нескольких часов работы. Одна из частых ошибок в воркер-пулах это утечка горутин.

❓ В чём проблема

Вот воркер-пул, который выглядит вполне разумно:
func startWorkers(jobs <-chan Job) {
for i := 0; i < 10; i++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
}


Каждый воркер читает канал jobs и обрабатывает всё, что приходит. Чистый идиоматичный Go. Проблема в том, что если jobs никто не закрывает, все эти горутины блокируются навсегда.

Цикл for range по каналу блокируется, пока канал не закрыт или пока не пришло значение. Если продюсер перестал слать данные, но не вызвал close(jobs), ваши 10 воркеров висят в памяти бесконечно. Они держат стек и не видны в метриках, пока вы явно не считаете число горутин.

В долгоживущем сервисе это накапливается. Каждый раз, когда вы перезапускаете пул, например при перезагрузке конфига или новой пачке задач, не дренируя старый, вы добавляете утёкшие горутины.

➡️ Как чинить

Продюсер закрывает канал, когда закончил, и эта ответственность лежит только на нём.
func runBatch(jobList []Job) {
jobs := make(chan Job, len(jobList))
// Продюсер владеет каналом и закрывает его по завершении
go func() {
defer close(jobs) // сработает даже при панике
for _, job := range jobList {
jobs <- job
}
}()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
process(job)
}
}()
}
wg.Wait()
}


Здесь два важных момента:

• во-первых, defer close(jobs) в горутине продюсера закрывает канал, даже если продюсер вышел раньше времени или упал с паникой при отправке задач.

• во-вторых, sync.WaitGroup даёт чистую точку синхронизации. Вызов wg.Wait() блокируется, пока все воркеры не дочитают канал.

Продюсер владеет каналом и закрывает его, всегда через defer. Быстрая диагностика на проде такая. Добавьте runtime.NumGoroutine() в health-эндпоинт. Если это число растёт за время жизни сервиса и не возвращается обратно, у вас утечка. В тестах самый простой способ это многократно гонять пул и следить за ростом числа горутин через runtime.NumGoroutine().

📍 Навигация: Вакансии • Задачи • Собесы

🐸 Библиотека Go-разработчика

#GoToProduction
  • 👍 8
  • 🔥 2
  • ❤ 1
  • 👏 1
More from @goproglib
  1. Sep 26, 2026🔥 В Go 1.27 появился portable SIMD До этого SIMD-оптимизации в Go требовали архитектурног…
  2. Sep 25, 2026🤡🤡 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoGiggle
  3. Sep 25, 2026💡 Код работает. А data race уже есть В Go можно записать значение в одной горутине, прочи…
  4. Sep 23, 2026💥 TCP/IP: что происходит с данными в сети Когда Go-приложение отправляет данные по сети,…
  5. Sep 22, 2026🔴 Встроенные функции len, make, panic — встроенные функции Go, которые можно использовать…
  6. Sep 21, 2026🤩 Go с нуля: серия базовых шпаргалок Собрали путь от Hello, World! до goroutines, channel…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →