Deep Engineering

ЗАМЕР

bench/goio/example.sh

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/io
Как запустить
go run bench/goio/internals.go
go run bench/goio/practice.go
bash bench/goio/example.sh

Запись прогона

Замеры для урока «io.Reader и io.Writer»

Файл Что делает
internals.go восемь наблюдений: два допустимых конца потока (последний кусок с nil и затем (0, EOF) — или вместе с EOF), цикл, который проверяет ошибку раньше n и теряет хвост, короткое чтение и io.ReadFull, буфер io.Copy и обход через WriterTo/ReaderFrom (счётчики вызовов и len(p)), какие типы стандартной библиотеки реализуют эти интерфейсы, MultiWriter/TeeReader/LimitReader/Pipe и память на копирование через них, короткая запись, bufio.Writer и Flush (в том числе поверх /dev/full), gzip без Close, defer f.Close() и потерянная ошибка
example.sh собирает и запускает полный короткий пример cases/example/main.go (свой go.mod, go 1.24): цикл чтения по контракту Read и запись через bufio с проверкой Flush; добавлен после внешнего аудита 14.10.2026
cases/example/ тот самый пример: его можно скопировать в пустой каталог и запустить go run .
practice.go источник ответов двух практических задач урока: вывод цикла, io.ReadFull и bufio без Flush; кратность времени 10 000 записей по 16 байт в файл без bufio и с ним
go run bench/goio/internals.go
go run bench/goio/practice.go
bash bench/goio/example.sh

Разделы 9 и 10 internals.go добавлены после внешнего аудита 14.10.2026 в конец вывода, чтобы не сдвигать процитированные блоки: полная запись вместе с ошибкой и данные к задачам (размер отчёта из раздела 8, итоговый срез io.ReadAll). Подписи о памяти в разделах 4 и 6 уточнены: это суммарный объём выделений (TotalAlloc), а не одновременно занятая память.

Записи прогонов — в runs/, go1.24.7 linux/amd64, GOMAXPROCS=2. runs/internals-go1.25.txt — тот же internals.go под go1.25.1: поведение совпало, кроме десятой доли килобайта в среднем объёме io.ReadAll (код ReadAll в io/io.go у двух версий одинаковый). Временная папка в выводе заменена на <папка>. Раздел 7 и раздел 8 пишут в /dev/full — это устройство Linux, на котором любая запись кончается ENOSPC.

Что получилось

Последний кусок может прийти вместе с EOF. strings.Reader, *os.File и io.Pipe отдают последние байты с err=nil, а EOF — следующим вызовом с n=0. iotest.DataErrReader отдаёт "ld" и EOF одним вызовом. Документация io.Reader разрешает оба поведения.

Цикл if err != nil { break } до обработки n теряет хвост — но только на втором виде читателя. На strings.Reader, файле и трубе он собирает "hello, world", на DataErrReader — "hello, wor": два байта потеряны без ошибки. Цикл «сначала n байт, потом ошибка» и io.ReadAll правы на всех.

Read не обязан заполнять буфер. HalfReader на буфер 8 отдаёт 4 байта, OneByteReader — 1, труба — столько, сколько положил один Write (3, 7, 2). io.ReadFull дочитывает до 8; на входе из 5 байт отдаёт n=5 и unexpected EOF, на пустом — n=0 и EOF. errors.Is(io.ErrUnexpectedEOF, io.EOF) — false.

io.Copy берёт буфер 32 КиБ — если не нашёл чужого метода. Из читателя с одним Read в писатель с одним Write 100 000 байт идут четырьмя Write ([32768 32768 32768 1696]), буфер — одно выделение на 32.0 КиБ. Из *bytes.Reader (WriterTo) — одним Write на 100 000 байт, Read не вызывается, выделений ноль. В *bytes.Buffer (ReaderFrom) читает сам буфер: len(p) 512, 512, 1024… — обёртка над источником прячет его WriterTo, но ReaderFrom приёмника io.Copy всё равно находит. io.LimitReader(src, 100) уменьшает буфер io.Copy до 100 байт.

Обёртки не копят данные. MultiWriter и TeeReader передают каждый кусок дальше в том же вызове; копирование 1 МиБ через MultiWriter — 2 выделения меньше килобайта, через TeeReader — 32.0 КиБ (буфер io.Copy), через io.Pipe — 32.5 КиБ. Для сравнения: io.ReadAll того же мегабайта выделяет 5118.3 КиБ — срез растёт добавлением. Писатель, вернувший n < len(p) без ошибки, общий цикл io.Copy превращает в short write (io/io.go, строки 443–446 на go1.24.7); источник в прогоне обёрнут в onlyReader, иначе ту же проверку сделал бы WriteTo у *strings.Reader.

bufio.Writer собирает записи в куски по 4096 байт. 1000 записей по 10 байт: без bufio нижний писатель получает 1000 Write, с bufio — 3 ([4096 4096 1808]), и третий — только после Flush; без него 1808 байт остались в буфере. Один Write больше буфера в пустой буфер идёт вниз напрямую. Поверх /dev/full все 100 Write в bufio вернули nil, ошибка no space left on device пришла только из Flush — и дальше каждый Write возвращает её же.

Close тоже пишет. Функция с defer f.Close() и bufio без Flush вернула nil, а файл на диске — 0 байт. gzip.Writer без Close отдал вниз 10 байт (заголовок), после Flush — 62, и оба раза чтение назад кончается unexpected EOF; после Close — 75 байт и 2600 байт обратно без ошибки. Повторный f.Close() — file already closed. Запись в /dev/full с defer w.Flush() вернула nil; с проверенным Flush и Close в defer, пишущим в именованный результат, — no space left on device; ошибку здесь дал Flush.

Буфер в этой конфигурации дал кратность 65. 10 000 записей по 16 байт прямо в *os.File во временной папке — 4248 мкс (10 000 Write дошли до файла), через bufio.Writer — 65 мкс (40 Write), лучший из 15 кругов, go1.24.7 linux/amd64. Это наблюдение одной машины: кратность зависит от файловой системы, ядра и размера записи, на другой машине её надо мерить.

Write может вернуть len(p) вместе с ошибкой. Контракт запрещает только короткую запись без ошибки. Писатель, вернувший len(p), errQuota, — io.Copy -> 12, err=quota exceeded и в общем цикле, и через WriteTo у *strings.Reader; bufio.Flush возвращает ту же ошибку. Писатель, вернувший len(p)/2, nil, — short write.

Сумма выделений — не живая память. 100 строк отчёта из раздела 8 — 1590 байт, меньше буфера bufio на 4096. io.ReadAll мегабайта суммарно выделил 5118.3 КиБ, а итоговый срез — cap=1114112; остальное — промежуточные массивы роста.

Скрипт

20 строк
#!/usr/bin/env bash
#
# Полный короткий пример урока: файл cases/example/main.go со своим go.mod
# (go 1.24), который можно скопировать в чистый каталог и запустить
# командой go run . — без зависимостей от остальных файлов bench/goio.
#
# ЗАЧЕМ. Добавлен после внешнего аудита 14.10.2026: остальные программы
# каталога — стенды наблюдений, их вывод не заменяет запускаемого примера.
#
# ЗАПУСК:
#
#	bash bench/goio/example.sh
set -u

cd "$(dirname "$0")/cases/example"

echo "$(go env GOVERSION) $(go env GOOS)/$(go env GOARCH) | полный пример: go run ."
echo
go run .