Как избежать утечек памяти при работе с файлами в цикле в Go
Что проверяют
Знаете ли вы, что defer срабатывает при выходе из функции, а не из итерации цикла, и отличаете ли утечку памяти от утечки файловых дескрипторов. Следом обычно просят показать, как такую утечку найти в работающем сервисе.
Как отвечать
Начните с главной ловушки. defer f.Close() внутри for откладывает закрытие до конца всей функции: на тысяче файлов вы держите тысячу открытых дескрипторов и упираетесь в ulimit -n с ошибкой too many open files. Память тут вторична, настоящий дефицит это дескрипторы. Как исправить и что добавить:
- тело цикла в отдельную функцию:
processFile(name) error, внутриdefer f.Close(), и файл закрывается на каждой итерации; - явный
Close(): безdefer, но тогда закрывать нужно на каждом пути выхода, включая ошибки; - ошибка закрытия: при записи
Close()может вернуть ошибку сброса данных, её не стоит терять, помогает именованный возврат илиerrors.Join; - потоковое чтение:
bufio.Scannerилиio.Copyвместоos.ReadFileна больших файлах, иначе файл целиком оказывается в куче; - переиспользование буфера: один
[]byteна весь цикл илиsync.Pool, если файлы обрабатываются параллельно.
Про диагностику: net/http/pprof и go tool pprof с профилями heap и goroutine, число открытых дескрипторов через ls /proc/<pid>/fd или lsof -p. Если файлы читают горутины, ограничьте их число семафором или errgroup с SetLimit, иначе тысяча горутин одновременно откроет тысячу файлов.
Где спотыкаются
Пишут defer file.Close() в цикле и считают, что файл закроется после итерации. Это ровно та утечка, о которой спрашивают.
Игнорируют результат Read: читают в общий буфер, не глядя на возвращённое n, и обрабатывают хвост от прошлого файла. Заодно забывают, что io.EOF может прийти вместе с последними байтами.
Надеются на сборщик мусора: «GC сам всё уберёт». Он освобождает память, но не закрывает файлы вовремя. Автоматическое закрытие при сборке у os.File есть, однако когда оно сработает, неизвестно, и рассчитывать на него нельзя.