Введение
В мире современной разработки на языке Go (Golang) горутины являются одной из ключевых особенностей, обеспечивающих высокую производительность и эффективность многозадачности. Они представляют собой легковесные потоки, которые позволяют выполнять задачи параллельно, что делает Go идеальным выбором для создания высоконагруженных приложений. Однако, как и любая мощная технология, горутины требуют внимательного подхода к управлению ресурсами. Одной из распространённых проблем, с которой сталкиваются разработчики, является утечка горутин — ситуация, когда горутины продолжают существовать и потреблять ресурсы, даже если они больше не нужны.
Утечка горутин может привести к серьёзным последствиям: от увеличения потребления памяти до полной остановки приложения из-за исчерпания системных ресурсов. В этой статье мы рассмотрим, что такое утечка горутин, как её обнаружить, какие инструменты и методики помогут предотвратить эту проблему, а также разберём практические примеры и лучшие практики для написания безопасного и эффективного кода на Go.
А что собственно происходит?
Утечка горутин в Go происходит, когда горутина продолжает существовать и потреблять ресурсы, даже если она больше не выполняет полезной работы или не может завершиться. Это может произойти по разным причинам.
Мы рассмотрим 3 примера из которых: 2 будут на каналах, 1 с использованием mutex.
Пример №1 - запись канал
func func1() int64 {
time.Sleep(2 * time.Second)
return 1
}
Обычная задача , мы хотим обернуть функцию func1 в декоратор и использовать context для возврата ошибки по истечению времени. Совсем просто сказал я и создал этот код.
func func2WithContext(ctx context.Context) (int64, error) {
ch := make(chan int64)
go func() { // goroutine try to write result in channel
ch <- func1()
}()
select {
case value := <-ch:
return value, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
Казалось бы, всё работает - мы прокидываем контест и ожидаем либо возврата от func1, либо истечения времени ctx.
Но представим, что ctx завершился раньше. Тогда вслед за ctx.Done() мы выйдем из func2WithContext, а func1 всё еще не закончит своё выполнение.
Спустя некоторое время func1 смогла вернуть результат обозначим его result , тогда анонимная горутина попытается записать result в наш канал, тут нам и хана.
Произойдет блокировка, так как канал не буферизированный и его уже давно никто не читает, анонимная горутинка зависнет.
Решение этого довольно легкое. Давайте просто сделаем буферизированный канал, тогда анонимная горутина сможет записать в него значение, а дальше сам канал удалится благодаря сборщику мусора.
func func2WithContext(ctx context.Context) (int64, error) {
ch := make(chan int64, 1)
go func() { // goroutine try to write result in channel
ch <- func1()
}()
select {
case value := <-ch:
return value, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
Пример №2 - чтение из канала
У нас есть следующий код. Легенда: некоторый сервис обрабатывает набор данных и отдает их в канал для дальнейшей работы.
func printData(dataCh chan string) { //goroutine wait info from dataCh
for info := range dataCh {
fmt.Println(info)
}
}
func processData(data []string) {
dataCh := make(chan string)
go printData(dataCh)
for _, info := range data {
dataCh <- info
}
}
Тут мы совершили страшный проступок ошибка и ты ошибся.
Если в каналы пишем конечный набор данных , а не используем их для бесконечной отдачи данных другой горутины, то канал должен закрываться
Заметим, что функция printData завершается только в том случае, если канал закрылся.
Функция processData не закрывает канал в который пишет, что в данном случае и приводит к утечке горутины и утечке объекта dataCh.
Решение довольно простое: нужно закрыть канал
func processData(data []string) {
dataCh := make(chan string)
go printData(dataCh)
for _, info := range data {
dataCh <- info
}
close(dataCh)
}
Пример №3 - блокировка блокированного Мьютекса
Вот же лексический повтор.
Вкратце мы имеем следующий код.
func work() {
m := &sync.Mutex{}
m.Lock()
go func() { // try to lock locked mutex
m.Lock()
}()
}
Заметим, что код максимально простой , тут мы всё что делаем это блокируем уже заблокированный Мьютекс внутри другой горутины.
Что имеем? Анонимная функция не будет закончена, пока ей не удастся залочить mutex.
Результат: у нас утекла горутина (
Как отладить?
Легко - никак
Проблема на самом деле острая и прийдется покопаться в грязи.
Для отладки есть несколько способов.
1) Линтер и проверка кода глазками.
В описанных выше случаях линтер не помогает, он не находит проблему, всё что мне остается - это искать глазками
2)Метрики и скрины стэк трейса.
Этот служит сужение круга подозреваемых, для дальнейшей отладки ручками. В основном все использует передачу метрик например в Grafana и debug с помощью скринов сервиса. Уже предустановленным пакетом pprof.
Советы - их будет 3
1) Следите за code style программы. Бейте большие участки кода на маленькие, что позволит сузить круг подозреваемых объектов
2) Убедитесь, что использованный объекты вы обработали правильно. Будь, то ctx и отменной Deadline или закрытие Канала
3) Набирайтесь опыта: благодаря опыту вы будете не допускать ошибок, просто потому-что вы помните как отлаживали подобное ... и не имеете желания делать это вновь.
PS
Моя первая техническая статья , так -что без буллинга, а то налетят Яндексишкины и накидают -rep, поймите карма не бесконечная. Я попытался максимально кратко обозначить проблему и дать мини ответы для пути её решения. Расписать всё подробно = статья на 30-60 минут, ее тогда никто не будет читать. Главное, что я хочу донести: боец осведомлён, что бывает утечка горутин, а как ее решать подробно, боец и без меня поймет. Иначе, не стать ему генералом.
Всех обнял.
Ссылка на исходники, там более подробно описано решение с помощью вывода стек-трейсов в рантайме.
- гитхаб с исходниками
- канальчик для вкатунов ВЕК
- личный ютуб бложек про ваше IT
Комментарии (4)
evgeniy_kudinov
12.02.2025 22:111.Наверное КДПВ лучше сменить на что-то нейтральное
2.Как выше уже указали, есть defer, и я дополню, что лучше, чтобы сразу после выделения ресурса стараться размещать defer (ресурс).Close(). «Глазками» проще увидеть и перестать беспокоиться о закрытии ресурса.r.Open() defer r.Close()
Часто видел, что, например, в одной ветке исполнения кода закрыли ресурс, а в другой забыли.r.Open() if .... { ..... r.Close() return } else if .... { .... return } else { ... } r.Close()
outlingo
В старых сложных немодных языках было правило - если ты создаешь ресурс, ты обязан его закрыть либо вернуть наружу чтобы его закрыл другой (это про канал, если что), если у тебя есть разделяемый ресурс к которому нельзя обращаться в некоторых стейтах - прикрой его оберткой.
А что до Go, у вас же в нем есть прекрасная конструкция defer - m.Lock() и тут же defer m.Unlock(), c := make(chan ...) и сразу defer Close(c)
Все давно придумано, надо только использовать.
egorChuyko Автор
Во всем правы , я постарался упомянуть об этом в статье.
Но бывают заурядные кейсы с мьютексами. defer m.Unlock() иногда может не использоваться , как например в исходниках go для оптимизация и быстрого разлочивания. Именно в продакшене не встречал , но в личных целях приходилось.
С каналами чуть сложнее , ведь не всегда есть желания закрывать по завершению функции , иногда приходится распаралелить запись в канал и придумывать сценарий , где только одна горутина его закроет.
qrKot
Вот так вот не надо, не надо сценариев, когда один из писателей закрывает канал. И придумывать такие сценарии тоже не надо, все придумано до нас: sync.WaitGroup - канал должен закрываться тогда и только тогда, когда мы гарантированно не собираемся в него больше писать