Как отслеживать и диагностировать утечки памяти в Go?

Ответ

Диагностика утечек памяти в Go — это систематический процесс, основанный на использовании встроенного инструментария.

Основные причины утечек памяти в Go:

  1. Незавершённые горутины (goroutine leaks): Горутины, которые блокируются навсегда (например, при чтении из пустого канала), не освобождают свой стек и связанные с ним ресурсы.
  2. Бесконтрольно растущие коллекции: Срезы (slice) или карты (map), которые используются как глобальные кэши или буферы и постоянно растут без очистки.
  3. Незакрытые ресурсы: Забытые defer file.Close() или defer resp.Body.Close() приводят к утечке файловых дескрипторов и памяти.
  4. Ссылки в замыканиях: Переменные, "захваченные" долгоживущими замыканиями, не могут быть собраны сборщиком мусора.

Инструменты и процесс диагностики:

Основной инструмент для анализа памяти — это встроенный профилировщик pprof.

Шаг 1: Подключение pprof к приложению

Для веб-серверов это делается очень просто:

import (
    _ "net/http/pprof" // Анонимный импорт для регистрации обработчиков
    "net/http"
    "log"
)

func main() {
    // ... ваш основной код

    // Запускаем pprof сервер на отдельном порту
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // ...
}

Шаг 2: Сбор профиля памяти (heap profile)

Когда приложение поработало и, предположительно, "накопило" лишнюю память, можно снять профиль:

# Снимаем профиль использования памяти (in-use space)
go tool pprof http://localhost:6060/debug/pprof/heap

Шаг 3: Анализ профиля

После запуска команды вы попадаете в интерактивную консоль pprof. Основные команды:

  • top: Показывает функции, которые удерживают больше всего памяти.
  • list <имя_функции>: Показывает исходный код функции с указанием, на какой строке выделяется память.
  • web: Генерирует и открывает в браузере визуальный граф вызовов. Это самый наглядный способ найти источник утечки.

Альтернативные способы:

  • Тесты с флагом -memprofile:

    go test -memprofile=mem.out ./...
    go tool pprof mem.out

    Это помогает находить утечки на ранних этапах, в рамках CI/CD.

  • runtime.ReadMemStats: Позволяет программно получать базовую статистику о памяти (Alloc, HeapObjects и т.д.) и отправлять её в систему мониторинга для отслеживания динамики потребления памяти со временем.

Ответ 18+ 🔞

А, ну ты про утечки памяти в Go, да? Ёпта, классика жанра! Сидишь такой, пишешь код, всё вроде летает, а потом — бац! — приложение жрёт память, как пиздопроебибна, и сервак накрывается медным тазом. Чисто русская рулетка, только с гигабайтами.

Так, слушай сюда, разбираемся, откуда ноги растут. Основные причины, блядь, почему твоя программа превращается в свинью-обжору:

  1. Горутины-зомби. Запустил ты её, сука, в фоне, а она взяла и зависла на вечном чтении из пустого канала. И сидит, блядь, как призрак, память свою не отпускает. Мёртвая, но не сдохшая.
  2. Коллекции-монстры. Глобальный кэш или мапа, которые ты забыл почистить. Они растут, как опухоль, и жрут всё подряд. «Ой, я потом, блядь, почищу» — а потом уже овердохуища гигов в оперативке.
  3. Ресурсы-сироты. Файл открыл, HTTP-ответ получил, а закрыть забыл. Дескрипторы кончаются, память течёт. Классический распиздяйский подход.
  4. Замыкания-ловушки. Переменную в замыкание захватил, а оно живёт дольше, чем хотелось. И тащит за собой на хуй кучу мусора, который сборщик мусора не трогает.

Ну и как с этим, блядь, бороться? Есть же у нас встроенный патрон — pprof!

Шаг первый: Подключаем пушку. В своё веб-приложение, сука, добавляешь одну строчку импорта, и всё. Проще некуда.

import (
    _ "net/http/pprof" // Вот эта магия, блядь! Анонимный импорт, и всё готово.
    "net/http"
    "log"
)

func main() {
    // ... тут твой гениальный код ...

    // А на отдельном порту запускаешь сервер для отладки.
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ...
}

Шаг второй: Стреляем по памяти. Приложение поработало, нажралось памяти. Теперь идёшь в консоль и снимаешь слепок, как патологоанатом.

# Берёшь профиль текущей, блядь, используемой памяти.
go tool pprof http://localhost:6060/debug/pprof/heap

Шаг третий: Читаем, как в книжке. Зашёл в интерактивную консоль pprof. Там команды простые, как три копейки:

  • top — покажет, какие функции у тебя самые прожорливые. Сразу видно, кто главный паразит.
  • list <имя_функции> — разжуёт по строчкам, где конкретно память выделяется. Прямо в исходниках покажет, сука, строку-убийцу.
  • web — вообще красота! Нарисует граф вызовов и откроет в браузере. Ты смотришь на эту картинку и сразу: «Ааа, вот же ты, пидарас шерстяной!»

А если тесты? Да похуй, и там можно!

go test -memprofile=mem.out ./...
go tool pprof mem.out

Запустил тесты с флагом, получил профиль — ищи утечки ещё до того, как они ушли в продакшн и всё ебнули.

Ну и для параноиков есть runtime.ReadMemStats. Можно программно считывать статистику (Alloc, HeapObjects) и мониторить, как память растёт. Чтобы не охуеть потом от неожиданности.

Вот и вся наука, блядь. Не так страшен чёрт, как его малюют. Главное — не бояться запускать pprof и смотреть правде в глаза. А то бывает, чувак, подозрение ебать чувствую, что что-то не так, но лень проверить. А потом — пиздец!