В этой статье я расскажу, что делать при обнаружении утечки в Go-приложении: чем могут быть вызваны утечки и с чего начать поиск источника проблемы. Для начала перечислим возможные причины утечки памяти: 1) Утечка горутин При утечки горутины утекают также переменные, которые находятся в области ее видимости. Кроме того, стек горутины выделяется в куче. Пример программы с утечкой горутин: 2) Бесконечная запись в глобальные переменные Приложение может бесконечно писать в какую-нибудь глобальную мапу, в результате чего память будет утекать. Один раз я пытался найти утечку у приложения, которое использовало gorilla context. Особенность этой библиотеки в том, что при обработке http запроса она сохраняет указатель на запрос в глобальную мапу и не удаляет ключ мапы без явного указания в пользовательском коде. Начиная с Go 1.7, разработчики gorilla рекомендуют использовать http.Request.Context() Эта статья будет сосредоточена на поиске первых двух типов утечек как наиболее распространенных. 3) sync.Pool, Cgo Нельзя не упомянуть о sync.Pool и Cgo. Sync.PoolСогласно официальной документации, "A Pool is a set of temporary objects that may be individually saved and retrieved." Существует интересная issue, при которой большие буферы вечно висят в пуле и со стороны ситуация выглядит как утечка. При использовании Cgo, необходимо самостоятельно заниматься менеджментом памяти, поэтому использовать его следует с осторожностью. Итак, разберемся с первыми двумя типами утечек – как их исправить? 1) Попытаться найти утечки глазами Внимательно пройтись по коду помогало мне довольно часто найти источник утечки. У Go есть особенность – все нужно закрывать. При использовании http клиента нужно закрывать response body. При выборке строк из базы данных – закрыть rows. При открытии файла – файл нужно закрыть. Это не всегда очевидно –  даже опытные программисты могут забыть про эти шаги, что и приводит к утечкам ресурсов. Пример кода с утечкой, где не закрыли http reponse body Пример кода с утечкой, где не закрыли rows 2) Применение профилировщика Если детальное изучение кода (см. Пункт 1) не помогло обнаружить источник утечки, переходим к применению профилировщика pprof. Согласно официальной документации, "pprof is a tool for visualization and analysis of profiling data. pprof reads a collection of profiling samples in profile.proto format and generates reports to visualize and help analyze the data. It can generate both text and graphical reports (through the use of the dot visualization package)." Для использования pprof нужно подключить в import "net/http/pprof". После этого по http порту, который слушает приложение станет доступен url "/debug/pprof/", по которому можно найти профили искомого pprof. Существует несколько полезных команд для использования pprof: Запустить web интерфейс с graph, флейм-графом и т.д.go tool pprof -http=:8081 http://localhost:9090/debug/pprof/goroutine Запустить интерактивный режим pprof.go tool pprof http://localhost:9090/debug/pprof/goroutineКоманда web откроет браузер по умолчанию с построенным графом Если профилировать нужно прод и нет возможности пройтись по url "/debug/pprof", можно скомпилировать приложение локально, попросить кого-нибудь с доступом к проду скачать профиль и запустить команду "go tool pprof  test pprof_goroutine" или "go tool pprof -http=:8081 test pprof_goroutine". test – это скомпилированный файл программы и pprof_goroutine – тот самый скачанный с прода профиль. Ранее среди причин утечки памяти я упомянул утечку горутин. По моему опыту, именно она является самой распространенной причиной, поэтому рекомендую в первую очередь проверить именно ее.  Профиль горутин этой программы после нескольких запросов к эндпоинту Если причина утечки – утечка коннектов, то можно будет увидеть постоянно растущий список файловых дескрипторов ls -la /proc/{pid}/fd. После этого необходимо посмотреть в профиль heap. Так выглядят код и профиль программы, которая бесконечно пишет в глобальную переменную. Профиль хипа после 2000 запросов 3) runtime.MemStats Первые два пункта выполнены, но утечки победить не удалось? Тогда есть смысл попробовать поднять приложение локально и использовать runtime.MemStats. Это структура, которая собирает статистику о работе аллокатора памяти. Например, сколько байтов для объектов в хипе аллоцировано, сколько таких объектов находится в хипе, сколько байтов занято активными спанами (единицы в Go для работы с виртуальной памятью, т.е. Go разбивает виртуальное адресное пространство кучи на спаны). Очень часто странное поведение приложения можно увидеть только на проде при определенных условиях. В этом случае стоит попытаться воссоздать эту ситуацию – поднять приложение локально, локализовать проблему до конкретного обработчика/участка кода, читать статистику MemStats. Заключение Здорово, если получилось быстро локализовать утечку, добавив или исправив несколько строк кода, но так бывает далеко не всегда и поиск может оказаться сложным. Иногда наиболее подходящим решением оказывается рефакторинг. Если понимаете, что существующий код неоптимально работает с памятью, использует асинхронную модель, успешное выполнение которой рандомизированно, то лучше отрефакторить его, чем тратить часы на трейсинг и дебаггинг. Полезные ссылки: Про pprofКак читать графProfiling & Optimizing in Go / Brad FitzpatrickGo Concurrency Patterns: Pipelines and cancellation