Кратко: goroutine — лёгкий поток выполнения в Go. Утечка горутины (goroutine leak) — это когда горутина больше не нужна, но остаётся заблокированной или работает вхолостую и не завершается, накапливая расход памяти/ресурсов. Ниже — простой пример утечки, способы диагностики и исправления. 1) Как работают goroutine (коротко) - Запускаются выражением `go f()` и выполняются асинхронно. - Коммуникация и синхронизация через каналы, контексты, WaitGroup и т.п. - Горутины дешевы, но не бесконечно: много заблокированных горутин потребляет память и может привести к проблемам. 2) Пример утечки (консольное приложение) leak.go: ``` package main import ( "fmt" "time" ) func worker(ch <-chan int) { for v := range ch { // если отправитель закроется — range завершится fmt.Println("work:", v) } // но если вместо закрытия отправитель просто перестанет писать — горутина может ждать } func main() { ch := make(chan int) // незавершённый канал for i := 0; i < 5; i++ { go worker(ch) // запускаем несколько рабочих горутин } // имитируем короткую работу и затем завершаем main time.Sleep(100 * time.Millisecond) fmt.Println("main done") // программа выходит, но если worker блокируется в другом сценарии — утечка } ``` Проблема в этом примере проявляется, когда горутины ожидают данные или сигнал завершения, а производитель завершился без корректного закрытия/сигнала — горутины остаются заблокированными. 3) Диагностика утечек - Быстрый способ: печатать количество горутин: ``` import "runtime" fmt.Println("goroutines:", runtime.NumGoroutine()) ``` - Снэпшот стека горутин: ``` import "runtime/pprof" pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) ``` - В web-аппликации: подключить net/http/pprof и открыть `/debug/pprof/goroutine?debug=2`. - Профайлер: `go tool pprof` при сборке профилей CPU/heap/ goroutine. - Частые признаки: постоянно растущее `runtime.NumGoroutine()`, много одинаковых стэктрейсов (ожидание на канале или select). 4) Частые причины утечек - Горутина ждёт на операции чтения/записи в канал, где другой конец не работает. - Блокировка в бесконечном select без ветки окончания. - Неотменённый контекст (context) при оперировании в фоне. - Не закрытые ресурсы (например, сокеты, файлы) — горутины, ожидающие их закрытия. 5) Исправление — пример с контекстом и корректной остановкой fixed.go: ``` package main import ( "context" "fmt" "sync" "time" ) func worker(ctx context.Context, id int, wg *sync.WaitGroup, jobs <-chan int) { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("worker", id, "stopping:", ctx.Err()) return case v, ok := <-jobs: if !ok { fmt.Println("worker", id, "jobs closed") return } // do work fmt.Println("worker", id, "got", v) } } } func main() { jobs := make(chan int) var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) // запускаем рабочих for i := 0; i < 5; i++ { wg.Add(1) go worker(ctx, i, &wg, jobs) } // отправляем несколько задач for i := 0; i < 10; i++ { jobs <- i } // корректно закрываем: либо закрыть канал, либо отменить контекст close(jobs) // это завершит worker-ы, если они читают из jobs // либо: cancel() // если используете контекст для остановки wg.Wait() cancel() fmt.Println("main done") } ``` Правила устранения утечек: - Дать явный сигнал остановки: закрывать каналы, отменять контексты (`cancel()`), посылать финальный маркер. - Внутри горутин использовать `select` с `<-ctx.Done()` или `case <-done:`. - При создании множества горутин применять пул воркеров (ограничивать параллелизм), WaitGroup для ожидания завершения. - Устанавливать таймауты: `context.WithTimeout` или `time.After` в select. - Не полагаться на GC для остановки фоновых операций. 6) Практическая диагностика шаги - Вставить `runtime.NumGoroutine()` в ключевые места для мониторинга. - Собрать дамп goroutine (pprof.Lookup("goroutine").WriteTo(...)) и проанализировать повторяющиеся стеки. - По стеку определить, на чём горутины блокируются (канал, sync.Cond, select и т.д.). - Добавить в код точки выхода (контексты/закрытие) и повторно проверить. Короткая памятка (checklist): - Всегда думайте, как горутина завершится. - Используйте context/WaitGroup/close(ch)/timeouts. - Мониторьте runtime.NumGoroutine и делайте дампы стэков при росте. Если хотите, могу дать минимальный пример демонстрации утечки с выводом `runtime.NumGoroutine()` и дампом стека, или помочь проанализировать конкретный стектрейс.
1) Как работают goroutine (коротко)
- Запускаются выражением `go f()` и выполняются асинхронно.
- Коммуникация и синхронизация через каналы, контексты, WaitGroup и т.п.
- Горутины дешевы, но не бесконечно: много заблокированных горутин потребляет память и может привести к проблемам.
2) Пример утечки (консольное приложение)
leak.go:
```
package main
import (
"fmt"
"time"
)
func worker(ch <-chan int) {
for v := range ch { // если отправитель закроется — range завершится
fmt.Println("work:", v)
}
// но если вместо закрытия отправитель просто перестанет писать — горутина может ждать
}
func main() {
ch := make(chan int) // незавершённый канал
for i := 0; i < 5; i++ {
go worker(ch) // запускаем несколько рабочих горутин
}
// имитируем короткую работу и затем завершаем main
time.Sleep(100 * time.Millisecond)
fmt.Println("main done")
// программа выходит, но если worker блокируется в другом сценарии — утечка
}
```
Проблема в этом примере проявляется, когда горутины ожидают данные или сигнал завершения, а производитель завершился без корректного закрытия/сигнала — горутины остаются заблокированными.
3) Диагностика утечек
- Быстрый способ: печатать количество горутин:
```
import "runtime"
fmt.Println("goroutines:", runtime.NumGoroutine())
```
- Снэпшот стека горутин:
```
import "runtime/pprof"
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
```
- В web-аппликации: подключить net/http/pprof и открыть `/debug/pprof/goroutine?debug=2`.
- Профайлер: `go tool pprof` при сборке профилей CPU/heap/ goroutine.
- Частые признаки: постоянно растущее `runtime.NumGoroutine()`, много одинаковых стэктрейсов (ожидание на канале или select).
4) Частые причины утечек
- Горутина ждёт на операции чтения/записи в канал, где другой конец не работает.
- Блокировка в бесконечном select без ветки окончания.
- Неотменённый контекст (context) при оперировании в фоне.
- Не закрытые ресурсы (например, сокеты, файлы) — горутины, ожидающие их закрытия.
5) Исправление — пример с контекстом и корректной остановкой
fixed.go:
```
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup, jobs <-chan int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("worker", id, "stopping:", ctx.Err())
return
case v, ok := <-jobs:
if !ok {
fmt.Println("worker", id, "jobs closed")
return
}
// do work
fmt.Println("worker", id, "got", v)
}
}
}
func main() {
jobs := make(chan int)
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// запускаем рабочих
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(ctx, i, &wg, jobs)
}
// отправляем несколько задач
for i := 0; i < 10; i++ {
jobs <- i
}
// корректно закрываем: либо закрыть канал, либо отменить контекст
close(jobs) // это завершит worker-ы, если они читают из jobs
// либо: cancel() // если используете контекст для остановки
wg.Wait()
cancel()
fmt.Println("main done")
}
```
Правила устранения утечек:
- Дать явный сигнал остановки: закрывать каналы, отменять контексты (`cancel()`), посылать финальный маркер.
- Внутри горутин использовать `select` с `<-ctx.Done()` или `case <-done:`.
- При создании множества горутин применять пул воркеров (ограничивать параллелизм), WaitGroup для ожидания завершения.
- Устанавливать таймауты: `context.WithTimeout` или `time.After` в select.
- Не полагаться на GC для остановки фоновых операций.
6) Практическая диагностика шаги
- Вставить `runtime.NumGoroutine()` в ключевые места для мониторинга.
- Собрать дамп goroutine (pprof.Lookup("goroutine").WriteTo(...)) и проанализировать повторяющиеся стеки.
- По стеку определить, на чём горутины блокируются (канал, sync.Cond, select и т.д.).
- Добавить в код точки выхода (контексты/закрытие) и повторно проверить.
Короткая памятка (checklist):
- Всегда думайте, как горутина завершится.
- Используйте context/WaitGroup/close(ch)/timeouts.
- Мониторьте runtime.NumGoroutine и делайте дампы стэков при росте.
Если хотите, могу дать минимальный пример демонстрации утечки с выводом `runtime.NumGoroutine()` и дампом стека, или помочь проанализировать конкретный стектрейс.