← Назад к вопросам
Найти баг: Goroutine Leak
2.0 Middle🔥 241 комментариев
#Конкурентность и горутины#Производительность и оптимизация
Условие
Найдите проблему утечки горутин в следующем коде:
package main
import (
"fmt"
"time"
)
func process() <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
}()
return ch
}
func main() {
ch := process()
for i := 0; i < 5; i++ {
fmt.Println(<-ch)
}
fmt.Println("done")
}
Вопросы
- Какая проблема в этом коде?
- Как исправить код, чтобы горутина корректно завершалась?
- Покажите решение с использованием context
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Это классическая задача на goroutine leak — утечку горутин. Горутина остаётся запущенной даже после того, как её результаты больше не нужны.
1. Какая проблема?
Проблема: Goroutine Leak (утечка горутин)
Горотина запущенная в process() никогда не завершается:
for i := 0; ; i++ { // ← бесконечный цикл
ch <- i
time.Sleep(100 * time.Millisecond)
}
В main читаются только 5 значений, затем программа выводит "done" и завершается, но горутина остаётся в памяти до выхода программы.
Последствия:
- Утечка памяти (горутина занимает память)
- Утечка системных ресурсов
- В больших системах может привести к exhaust всех горутин
2. Как исправить?
Вариант 1: Закрыть канал
func process(count int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // ← гарантирует закрытие
for i := 0; i < count; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
}()
return ch
}
func main() {
ch := process(5)
for val := range ch { // ← range автоматически завершится
fmt.Println(val)
}
fmt.Println("done")
}
Вариант 2: Передать количество
func process(count int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < count; i++ // ← конечный цикл
ch <- i
time.Sleep(100 * time.Millisecond)
}
}()
return ch
}
3. Решение с context (РЕКОМЕНДУЕТСЯ)
Это самый правильный способ:
package main
import (
"context"
"fmt"
"time"
)
func process(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // гарантирует закрытие канала
i := 0
for {
select {
case <-ctx.Done(): // ← проверяем отмену
return // горутина корректно завершается
case ch <- i:
i++
time.Sleep(100 * time.Millisecond)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // гарантирует отмену
ch := process(ctx)
for i := 0; i < 5; i++ {
fmt.Println(<-ch)
}
cancel() // ← отменяем контекст, горутина завершается
time.Sleep(200 * time.Millisecond) // ждём завершения
fmt.Println("done")
}
Почему это лучше:
- Явно сигнализирует горутине о завершении
- Работает с таймаутами
- Cascading cancellation в иерархии
- Production-ready подход
Полное решение с context
package main
import (
"context"
"fmt"
"time"
)
func process(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
i := 0
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop() // гарантирует остановку ticker
for {
select {
case <-ctx.Done(): // отмена контекста
fmt.Println("Горутина завершена")
return
case <-ticker.C: // каждые 100ms
select {
case ch <- i: // отправить значение
i++
case <-ctx.Done(): // проверить ещё раз
return
}
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
1 * time.Second, // таймаут 1 секунда
)
defer cancel()
ch := process(ctx)
for val := range ch {
fmt.Println("Значение:", val)
if val >= 4 {
break // выходим после 5 значений
}
}
cancel() // отменяем
time.Sleep(200 * time.Millisecond) // ждём завершения
fmt.Println("done")
}