TGViewer
Golang Golang @golang_google · 40.5K subscribers
Post #3598 8.34K
👣 `os.ReadFile()` действительно читает весь файл в память. Но RSS может показать почти ноль - и вот почему

Есть интересный эксперимент: читаем файл на 500 МБ через os.ReadFile(), ставим программу на паузу и смотрим RSS процесса.


b, err := os.ReadFile("bigfile.txt")
if err != nil {
panic(err)
}

fmt.Println(len(b))
fmt.Scanln()


Ожидаем около 500 МБ RSS.

А видим условные 3 МБ.

На первый взгляд кажется, что os.ReadFile() ничего не загрузил. Но причина совсем другая.

[]byte - это только descriptor:


slice
├─ ptr ───────────┐
├─ len │
└─ cap │

+-------------------+
| 500 MB byte array |
+-------------------+


Сам slice header на 64-битной системе занимает всего несколько машинных слов. Основные 500 МБ находятся в backing array на heap.

Ключевой момент - liveness analysis.

После:


fmt.Println(len(b))
fmt.Scanln()


переменная b больше не используется.

Для Go это означает, что объект может перестать считаться live ещё до выхода main(). GC получает право освободить backing array, пока программа всё ещё ждёт Enter.

Добавим обращение после паузы:


fmt.Scanln()

fmt.Println(b[0])


И RSS внезапно становится примерно равен размеру файла.

Это хороший пример того, почему:

scope переменной != время жизни объекта для GC.

Чтобы явно сохранить объект живым до определённой точки, в низкоуровневом коде существует:


runtime.KeepAlive(b)


Но использовать KeepAlive просто ради красивого RSS обычно не нужно - он важен прежде всего при работе с finalizer, unsafe, syscall и foreign memory.

Ещё одна ловушка - go run.


go run main.go bigfile.txt


сначала компилирует временный binary, а затем запускает его отдельным процессом. Поэтому смотреть память нужно у процесса вида:


/tmp/go-build.../exe/main


а не у самого go run.

Практический вывод для production:


data, _ := os.ReadFile(path)


подходит для небольших файлов, но большой файл означает большую аллокацию, давление на heap и GC.

Для потоковой обработки лучше:


f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()

r := bufio.NewReaderSize(f, 256<<10)


или вообще:


_, err = io.Copy(dst, f)


Тогда объём рабочей памяти зависит от размера буфера, а не от размера файла.

Главный урок: RSS показывает состояние процесса в конкретный момент, а не полную историю его аллокаций.

И если профилируете память Go-приложения, одного ps мало - смотрите ещё heap profile, runtime.MemStats и GODEBUG=gctrace=1.
  • 👍 32
  • ❤ 11
  • 🔥 6
More from @golang_google
  1. Sep 21, 2026🛠 Ax - фреймворк в стиле DSPy для Go, TypeScript, Python, Java, C++, и Rust Вы описываете…
  2. Sep 18, 2026🔥 Rune - быстрый IDE и terminal multiplexer с GPU-рендерингом, который теперь открыт на G…
  3. Sep 16, 2026🔥 Большой гайд по миграции с Go на Rust Для разработчиков переход обычно упирается не в с…
  4. Sep 15, 2026🔥 Yoke - управление пакетами Kubernetes без гор YAML Вместо сложных Helm-шаблонов Yoke по…
  5. Sep 13, 2026🔥 Yzma - прямое подключение `llama.cpp` к Go-приложениям для локального inference Проект…
  6. Sep 12, 2026🐳 Как правильно использовать Docker для Go-приложения - пошаговый гайд Хороший разбор от…
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 →