TGViewer
Библиотека Go-разработчика | Golang Библиотека Go-разработчика | Golang @goproglib · 24.1K subscribers
Post #7340 2.55K
📎 Разбираемся, как os.ReadFile() работает с памятью

Во многих туториалах по Go встречается фраза «os.ReadFile() читает весь файл в память». Мы привыкли принимать такие утверждения на веру, хотя проверить их легко.

В этом посте разберём, что на самом деле происходит с памятью процесса, когда os.ReadFile() обрабатывает файл на 500 мегабайт, и почему результат измерений может удивить.

➡️ Эксперимент

Берём простую программу. Она читает файл, путь к которому передан аргументом, выводит его размер и затем ждёт нажатия Enter, чтобы у нас было время посмотреть на потребление памяти процесса:
package main

import (
"fmt"
"os"
)

func main() {
if len(os.Args) != 2 {
fmt.Println("Please specify a path")
return
}

b, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
fmt.Println("File has been read into memory.")
fmt.Println("Size:", len(b), "bytes")

fmt.Println("Press Enter to exit...")
fmt.Scanln()
}


Сначала создаём файл на 500 мегабайт:
fallocate -l 500M bigfile.txt


Запускаем программу через go run:
go run readingFile_bad.go bigfile.txt


Пока программа стоит на паузе, смотрим на её RSS, то есть на объём физической памяти, реально занятой процессом:
ps -o pid,rss,vsz,cmd -p 228159
PID RSS VSZ CMD
228159 3116 1750908 /tmp/go-build2199976311/b001/exe/readingFile_bad bigfile.txt


RSS показывает примерно 3 мегабайта, хотя файл весит 500. Логично ожидать, что вся эта память должна быть занята. Разберёмся, почему этого не происходит.

❓ Почему память не занята

Когда os.ReadFile() вызывается, Go выделяет на куче backing array размером с файл, в нашем случае 500 мегабайт, и операционная система копирует туда содержимое файла с диска.

Переменная b []byte, которую мы получаем, это не сами данные. Это всего лишь заголовок слайса, маленькая структура размером около 24 байт на 64-битной системе, которая хранит три поля: указатель на первый байт backing array, текущую длину слайса, и полную ёмкость backing array.

Сам слайс маленький, а массив с данными файла занимает реальные 500 мегабайт где-то в куче:
b │
├── pointer ───────────────┐
├── length = 524288000 |
└── capacity = 524288000 │
▼
+-----------------------+
| 500 MB backing array |
+-----------------------+


Дело в том, что в нашей программе после fmt.Scanln() переменная b больше нигде не используется. Компилятор и сборщик мусора видят, что b мертва после этой точки, поэтому backing array становится недоступным для использования и сборщик мусора может его освободить ещё до того, как мы посмотрели на RSS.

Чтобы проверить эту гипотезу, раскомментируем строку с обращением к b[0] в конце программы:
fmt.Println("FirstByte: ", b[0])


Теперь компилятор знает, что переменная понадобится после Scanln(), и не может позволить сборщику мусора забрать её раньше времени. Запускаем программу заново и смотрим на RSS:
ps -o pid,rss,vsz,cmd -p 233685
PID RSS VSZ CMD
233685 514680 1750844 /tmp/go-build1663734078/b001/exe/readingFile_bad bigfile.txt


На этот раз RSS равен 514680 килобайт, то есть около 514 мегабайт. Это уже соответствует ожиданиям.

Стоит добавить ещё один нюанс. Команда go run сама по себе не запускает наш код напрямую, она сначала компилирует временный исполняемый файл во временную директорию и уже его запускает отдельным процессом. Поэтому при анализе памяти нужно смотреть не на процесс go run, а на дочерний процесс из /tmp/go-build.../exe/, именно он реально читает файл.

os.ReadFile() действительно выделяет память под весь файл целиком, утверждение из туториалов верное. Но []byte, который мы получаем, это лёгкий заголовок слайса, а не сами данные, и поведение сборщика мусора влияет на то, когда именно эта память физически занята процессом. Если переменная с данными становится недостижимой раньше, чем вы успели посмотреть на RSS, может показаться, что файл вообще не загружался в память, хотя на деле это просто сборка мусора отработала раньше, чем мы ожидали.

Полезный практический вывод из этого расследования простой. Если вам нужно работать с большими файлами, лучше избегать os.ReadFile() и использовать потоковое чтение через bufio.Reader или io.Copy, чтобы не держать весь файл в памяти разом.

📍 Навигация: Вакансии • Задачи • Собесы

🐸 Библиотека Go-разработчика

#GoDeep
  • 👍 13
  • ❤ 5
  • 🔥 1
More from @goproglib
  1. Sep 26, 2026🔥 В Go 1.27 появился portable SIMD До этого SIMD-оптимизации в Go требовали архитектурног…
  2. Sep 25, 2026🤡🤡 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoGiggle
  3. Sep 25, 2026💡 Код работает. А data race уже есть В Go можно записать значение в одной горутине, прочи…
  4. Sep 23, 2026💥 TCP/IP: что происходит с данными в сети Когда Go-приложение отправляет данные по сети,…
  5. Sep 22, 2026🔴 Встроенные функции len, make, panic — встроенные функции Go, которые можно использовать…
  6. Sep 21, 2026🤩 Go с нуля: серия базовых шпаргалок Собрали путь от Hello, World! до goroutines, channel…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →