ЗАМЕР
bench/goio/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /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; остальное —
промежуточные массивы роста.
Скрипт
572 строк//go:build ignore
// io.Reader и io.Writer: что на самом деле обещает контракт из двух методов.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. «Reader — это интерфейс с методом Read» знают все.
// Вопросы начинаются там, где код написан по образу strings.Reader, а
// работает с сетью, архивом или файлом: Read возвращает данные вместе с
// ошибкой, заполняет буфер не до конца, io.Copy делает работу чужими
// методами, а записанное через bufio и gzip доходит до файла только после
// Flush и Close. Каждый пункт здесь — запуск, а не пересказ документации.
//
// ВОСЕМЬ СЮЖЕТОВ:
//
// 1. Два допустимых конца потока: последний кусок с nil и затем (0, EOF),
// или последний кусок сразу вместе с EOF (iotest.DataErrReader).
// 2. Цикл, который проверяет err раньше n, теряет последний кусок — но
// только на втором виде читателя.
// 3. Короткое чтение: Read не обязан заполнять буфер; io.ReadFull обязан,
// и при нехватке данных отвечает io.ErrUnexpectedEOF.
// 4. io.Copy: буфер 32 КБ, WriterTo и ReaderFrom вместо буфера, LimitReader
// уменьшает буфер — видно по счётчикам вызовов и по len(p).
// 5. Какие типы стандартной библиотеки реализуют WriterTo и ReaderFrom.
// 6. Композиция: MultiWriter, TeeReader, LimitReader, Pipe — что делают и
// сколько памяти берёт копирование через них; короткая запись.
// 7. Буферизация: сколько Write доходит до нижнего писателя без bufio и с
// ним; без Flush хвост теряется; ошибка записи всплывает только на Flush.
// 8. Close тоже пишет: gzip без Close, bufio без Flush под defer f.Close(),
// повторный Close и шаблон, который не теряет ошибку.
//
// И два раздела, добавленные после внешнего аудита 14.10.2026 (в конец,
// чтобы не сдвигать процитированные в уроке блоки):
//
// 9. Write может вернуть n == len(p) вместе с ошибкой — контракт этого не
// запрещает; io.Copy и bufio такую ошибку передают наверх.
// 10. Данные к задачам: сколько байт в 100 строках отчёта из раздела 8 и
// какой срез io.ReadAll возвращает в конце — чтобы не путать суммарный
// объём выделений с итоговым буфером.
//
// Временные пути заменены плейсхолдером <папка>, чтобы вывод не менялся от
// запуска к запуску.
//
// ЗАПУСК:
//
// go run bench/goio/internals.go
package main
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"testing/iotest"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
var tmpDir string
// hide заменяет временную папку плейсхолдером: путь меняется от запуска к
// запуску, а вывод должен быть одинаковым.
func hide(s string) string { return strings.ReplaceAll(s, tmpDir, "<папка>") }
func errStr(err error) string {
if err == nil {
return "nil"
}
return hide(err.Error())
}
// ------------------------------------------------------------ счётчики
// onlyReader прячет у читателя все методы, кроме Read: так io.Copy не найдёт
// WriterTo и пойдёт общим путём через свой буфер.
type onlyReader struct {
r io.Reader
calls int
sizes []int // len(p) каждого вызова
}
func (o *onlyReader) Read(p []byte) (int, error) {
o.calls++
o.sizes = append(o.sizes, len(p))
return o.r.Read(p)
}
// countWriter считает вызовы Write и байты, ничего не храня.
type countWriter struct {
calls int
bytes int
sizes []int
}
func (c *countWriter) Write(p []byte) (int, error) {
c.calls++
c.bytes += len(p)
c.sizes = append(c.sizes, len(p))
return len(p), nil
}
// memPerOp — сколько выделено в куче за один вызов f: число выделений и
// байты, в среднем по rounds вызовам. Как testing.AllocsPerRun, на время
// замера ставит GOMAXPROCS=1, чтобы чужие горутины не добавляли шума.
func memPerOp(rounds int, f func()) (allocs, bytes uint64) {
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
f() // прогрев
runtime.GC()
var a, b runtime.MemStats
runtime.ReadMemStats(&a)
for i := 0; i < rounds; i++ {
f()
}
runtime.ReadMemStats(&b)
r := uint64(rounds)
return (b.Mallocs - a.Mallocs + r/2) / r, (b.TotalAlloc - a.TotalAlloc + r/2) / r
}
// kib печатает байты в КиБ с одним знаком: последние байты среднего
// дрожат от запуска к запуску, а десятые доли килобайта — нет.
func kib(b uint64) string {
if b < 1024 {
return "< 1 КиБ"
}
return fmt.Sprintf("%.1f КиБ", float64(b)/1024)
}
// ------------------------------------------------------------ 1–2. EOF
func trace(name string, r io.Reader, size int) {
buf := make([]byte, size)
fmt.Printf(" %s\n", name)
for i := 1; ; i++ {
n, err := r.Read(buf)
fmt.Printf(" Read #%d -> n=%d %-10q err=%s\n", i, n, buf[:n], errStr(err))
if err != nil {
return
}
}
}
// readBroken — цикл, который сначала смотрит на ошибку.
func readBroken(r io.Reader) string {
var out []byte
buf := make([]byte, 5)
for {
n, err := r.Read(buf)
if err != nil {
break
}
out = append(out, buf[:n]...)
}
return string(out)
}
// readCorrect — цикл по документации: сначала n байт, потом ошибка.
func readCorrect(r io.Reader) (string, error) {
var out []byte
buf := make([]byte, 5)
for {
n, err := r.Read(buf)
out = append(out, buf[:n]...)
if err == io.EOF {
return string(out), nil
}
if err != nil {
return string(out), err
}
}
}
// chunked — тот же поток "hello, world", но кусками: через io.Pipe, в
// который писатель кладёт "hel", "lo, wor" и "ld" тремя вызовами Write.
func chunked() io.Reader {
pr, pw := io.Pipe()
go func() {
for _, part := range []string{"hel", "lo, wor", "ld"} {
pw.Write([]byte(part))
}
pw.Close()
}()
return pr
}
// ------------------------------------------------------- 6. короткая запись
// shortWriter нарушает контракт Writer: пишет половину и не возвращает ошибку.
type shortWriter struct{}
func (shortWriter) Write(p []byte) (int, error) { return len(p) / 2, nil }
// ------------------------------------------------------------ 8. Close
// reportLine — строка отчёта из writeReportDeferOnly; вынесена, чтобы раздел
// 10 мог посчитать, сколько байт она занимает.
func reportLine(i int) string { return fmt.Sprintf("строка %d\n", i) }
// writeReportDeferOnly — так часто пишут: defer f.Close(), буфер не сброшен.
func writeReportDeferOnly(path string, lines int) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
for i := 0; i < lines; i++ {
w.WriteString(reportLine(i))
}
return nil
}
// writeToDeferFlush — ошибка Flush отложена и потеряна.
func writeToDeferFlush(path string) error {
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
_, err = w.WriteString("отчёт\n")
return err
}
// writeToChecked — Flush и Close проверены, первая ошибка возвращается.
func writeToChecked(path string) (err error) {
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
w := bufio.NewWriter(f)
if _, err = w.WriteString("отчёт\n"); err != nil {
return err
}
return w.Flush()
}
func main() {
var err error
tmpDir, err = os.MkdirTemp("", "goio")
if err != nil {
panic(err)
}
defer os.RemoveAll(tmpDir)
fmt.Printf("%s %s/%s | io.Reader и io.Writer\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
const text = "hello, world"
path := filepath.Join(tmpDir, "hello.txt")
if err := os.WriteFile(path, []byte(text), 0o644); err != nil {
panic(err)
}
// ------------------------------------------------------------------ 1
show("1) ДВА ДОПУСТИМЫХ КОНЦА ПОТОКА")
fmt.Printf(" поток %q, 12 байт, буфер 5 байт\n", text)
trace("strings.NewReader", strings.NewReader(text), 5)
f, _ := os.Open(path)
trace("*os.File", f, 5)
f.Close()
trace("io.Pipe, писатель: Write(\"hel\"), Write(\"lo, wor\"), Write(\"ld\")", chunked(), 5)
trace("iotest.DataErrReader(strings.NewReader)", iotest.DataErrReader(strings.NewReader(text)), 5)
fmt.Println(" Оба поведения разрешены документацией io.Reader: последний кусок")
fmt.Println(" может прийти с err=nil, а EOF — следующим вызовом, или вместе с EOF.")
// ------------------------------------------------------------------ 2
show("2) ЦИКЛ, КОТОРЫЙ СНАЧАЛА СМОТРИТ НА ОШИБКУ")
fmt.Println(" if err != nil { break } до обработки n байт:")
fmt.Printf(" strings.NewReader -> %q\n", readBroken(strings.NewReader(text)))
f, _ = os.Open(path)
fmt.Printf(" *os.File -> %q\n", readBroken(f))
f.Close()
fmt.Printf(" io.Pipe (\"hel\", \"lo, wor\", \"ld\") -> %q\n", readBroken(chunked()))
fmt.Printf(" DataErrReader -> %q (потеряно %d байта)\n",
readBroken(iotest.DataErrReader(strings.NewReader(text))),
len(text)-len(readBroken(iotest.DataErrReader(strings.NewReader(text)))))
fmt.Println(" сначала n байт, потом ошибка:")
s, _ := readCorrect(strings.NewReader(text))
fmt.Printf(" strings.NewReader -> %q\n", s)
s, _ = readCorrect(chunked())
fmt.Printf(" io.Pipe (\"hel\", \"lo, wor\", \"ld\") -> %q\n", s)
s, _ = readCorrect(iotest.DataErrReader(strings.NewReader(text)))
fmt.Printf(" DataErrReader -> %q\n", s)
b, err := io.ReadAll(iotest.DataErrReader(strings.NewReader(text)))
fmt.Printf(" io.ReadAll(DataErrReader) -> %q err=%s\n", b, errStr(err))
// ------------------------------------------------------------------ 3
show("3) КОРОТКОЕ ЧТЕНИЕ: READ НЕ ОБЯЗАН ЗАПОЛНЯТЬ БУФЕР")
buf := make([]byte, 8)
n, err := iotest.HalfReader(strings.NewReader(text)).Read(buf)
fmt.Printf(" HalfReader.Read(buf[8]) -> n=%d %q err=%s\n", n, buf[:n], errStr(err))
n, err = iotest.OneByteReader(strings.NewReader(text)).Read(buf)
fmt.Printf(" OneByteReader.Read(buf[8]) -> n=%d %q err=%s\n", n, buf[:n], errStr(err))
pr := chunked()
fmt.Println(" io.Pipe, писатель пишет \"hel\", \"lo, wor\", \"ld\"; читатель — буфером 8:")
for {
n, err := pr.Read(buf)
fmt.Printf(" Read -> n=%d %q err=%s\n", n, buf[:n], errStr(err))
if err != nil {
break
}
}
n, err = io.ReadFull(iotest.HalfReader(strings.NewReader(text)), buf)
fmt.Printf(" io.ReadFull(HalfReader, buf[8]) -> n=%d %q err=%s\n", n, buf[:n], errStr(err))
n, err = io.ReadFull(strings.NewReader("hello"), buf)
fmt.Printf(" io.ReadFull(5 байт, buf[8]) -> n=%d %q err=%s\n", n, buf[:n], errStr(err))
n, err = io.ReadFull(strings.NewReader(""), buf)
fmt.Printf(" io.ReadFull(0 байт, buf[8]) -> n=%d err=%s\n", n, errStr(err))
fmt.Printf(" errors.Is(ErrUnexpectedEOF, EOF) -> %v\n", errors.Is(io.ErrUnexpectedEOF, io.EOF))
// ------------------------------------------------------------------ 4
show("4) IO.COPY: БУФЕР 32 КБ ИЛИ ЧУЖОЙ МЕТОД")
data := bytes.Repeat([]byte("x"), 100_000)
fmt.Println(" копируется 100000 байт")
src := &onlyReader{r: bytes.NewReader(data)}
dst := &countWriter{}
nn, _ := io.Copy(dst, src)
fmt.Printf(" только Read -> только Write: Read ×%d, len(p)=%d; Write ×%d %v; скопировано %d\n",
src.calls, src.sizes[0], dst.calls, dst.sizes, nn)
dst = &countWriter{}
nn, _ = io.Copy(dst, bytes.NewReader(data))
fmt.Printf(" *bytes.Reader (WriterTo): Read ×0; Write ×%d %v; скопировано %d\n",
dst.calls, dst.sizes, nn)
src = &onlyReader{r: bytes.NewReader(data)}
var bb bytes.Buffer
nn, _ = io.Copy(&bb, src)
fmt.Printf(" -> *bytes.Buffer (ReaderFrom): Read ×%d, первые len(p)=%v; скопировано %d\n",
src.calls, src.sizes[:3], nn)
src = &onlyReader{r: bytes.NewReader(data)}
dst = &countWriter{}
nn, _ = io.Copy(dst, io.LimitReader(src, 100))
fmt.Printf(" io.LimitReader(src, 100): Read ×%d, len(p)=%d; Write ×%d; скопировано %d\n",
src.calls, src.sizes[0], dst.calls, nn)
fmt.Println(" всего выделено в куче (TotalAlloc) за один io.Copy, среднее по 1000:")
cw := &countWriter{sizes: make([]int, 0, 8)}
or := &onlyReader{sizes: make([]int, 0, 8)}
br := bytes.NewReader(data)
pa, pb := memPerOp(1000, func() {
br.Reset(data)
or.r, or.sizes = br, or.sizes[:0]
cw.sizes = cw.sizes[:0]
io.Copy(cw, or)
})
wa, wb := memPerOp(1000, func() {
br.Reset(data)
cw.sizes = cw.sizes[:0]
io.Copy(cw, br)
})
fmt.Printf(" общий путь (буфер io.Copy) выделений: %d, %s\n", pa, kib(pb))
fmt.Printf(" через WriterTo выделений: %d, %s\n", wa, kib(wb))
// ------------------------------------------------------------------ 5
show("5) КТО В СТАНДАРТНОЙ БИБЛИОТЕКЕ РЕАЛИЗУЕТ WRITERTO И READERFROM")
impl := func(name string, v any) {
_, wt := v.(io.WriterTo)
_, rf := v.(io.ReaderFrom)
fmt.Printf(" %-18s WriterTo: %-5v ReaderFrom: %v\n", name, wt, rf)
}
impl("*strings.Reader", strings.NewReader(""))
impl("*bytes.Reader", bytes.NewReader(nil))
impl("*bytes.Buffer", &bytes.Buffer{})
impl("*bufio.Reader", bufio.NewReader(nil))
impl("*bufio.Writer", bufio.NewWriter(io.Discard))
f, _ = os.Open(path)
impl("*os.File", f)
f.Close()
impl("*net.TCPConn", &net.TCPConn{})
impl("*io.PipeReader", pr)
impl("*io.LimitedReader", &io.LimitedReader{})
impl("io.Discard", io.Discard)
// ------------------------------------------------------------------ 6
show("6) КОМПОЗИЦИЯ: ОБЁРТКИ НЕ КОПЯТ ДАННЫЕ")
mb := bytes.Repeat([]byte("ab"), 1<<19) // 1 МиБ
fmt.Println(" поток 1048576 байт")
a1, a2 := &countWriter{}, &countWriter{}
io.Copy(io.MultiWriter(a1, a2), &onlyReader{r: bytes.NewReader(mb)})
fmt.Printf(" MultiWriter(a, b): a получил %d байт за %d Write, b — %d за %d\n",
a1.bytes, a1.calls, a2.bytes, a2.calls)
h := sha256.New()
d := &countWriter{}
io.Copy(d, io.TeeReader(&onlyReader{r: bytes.NewReader(mb)}, h))
want := sha256.Sum256(mb)
fmt.Printf(" TeeReader(r, sha256): записано %d байт, хеш совпал с sha256.Sum256: %v\n",
d.bytes, bytes.Equal(h.Sum(nil), want[:]))
d = &countWriter{}
io.Copy(d, io.LimitReader(bytes.NewReader(mb), 1000))
fmt.Printf(" LimitReader(r, 1000): записано %d байт\n", d.bytes)
pr2, pw2 := io.Pipe()
go func() {
pw2.Write(mb)
pw2.Close()
}()
d = &countWriter{}
io.Copy(d, pr2)
fmt.Printf(" Pipe, один Write на 1048576 байт: читатель получил %d байт за %d Write по %d\n",
d.bytes, d.calls, d.sizes[0])
fmt.Println(" всего выделено в куче (TotalAlloc) за одно копирование 1 МиБ, среднее по 200:")
mem := func(name string, f func()) {
a, b := memPerOp(200, f)
fmt.Printf(" %-30s выделений: %d, %s\n", name, a, kib(b))
}
sink1, sink2 := &countWriter{sizes: make([]int, 0, 64)}, &countWriter{sizes: make([]int, 0, 64)}
reset := func() { sink1.sizes, sink2.sizes = sink1.sizes[:0], sink2.sizes[:0] }
rd := bytes.NewReader(mb)
mem("io.Copy(w, *bytes.Reader)", func() { reset(); rd.Reset(mb); io.Copy(sink1, rd) })
mem("io.Copy(MultiWriter(w1, w2))", func() { reset(); rd.Reset(mb); io.Copy(io.MultiWriter(sink1, sink2), rd) })
mem("io.Copy(w, TeeReader)", func() { reset(); rd.Reset(mb); io.Copy(sink1, io.TeeReader(rd, sink2)) })
mem("io.Copy(w, io.Pipe)", func() {
reset()
r, w := io.Pipe()
go func() { w.Write(mb); w.Close() }()
io.Copy(sink1, r)
})
mem("io.ReadAll(*bytes.Reader)", func() { rd.Reset(mb); io.ReadAll(rd) })
// Источник обёрнут: иначе io.Copy ушёл бы в WriteTo у *strings.Reader,
// и short write выставил бы он, а не общий цикл io.Copy.
nn, err = io.Copy(shortWriter{}, &onlyReader{r: strings.NewReader(text)})
fmt.Printf(" писатель вернул n < len(p) и nil: io.Copy -> %d, err=%s\n", nn, errStr(err))
// ------------------------------------------------------------------ 7
show("7) БУФЕРИЗАЦИЯ: СКОЛЬКО WRITE ДОХОДИТ ДО НИЖНЕГО ПИСАТЕЛЯ")
fmt.Println(" 1000 записей по 10 байт = 10000 байт")
rec := []byte("0123456789")
low := &countWriter{}
for i := 0; i < 1000; i++ {
low.Write(rec)
}
fmt.Printf(" без bufio: Write внизу ×%-4d дошло байт: %5d\n", low.calls, low.bytes)
low = &countWriter{}
bw := bufio.NewWriter(low)
for i := 0; i < 1000; i++ {
bw.Write(rec)
}
fmt.Printf(" bufio.NewWriter, до Flush: Write внизу ×%-4d дошло байт: %5d, в буфере: %d\n",
low.calls, low.bytes, bw.Buffered())
bw.Flush()
fmt.Printf(" после Flush: Write внизу ×%-4d дошло байт: %5d %v\n", low.calls, low.bytes, low.sizes)
fmt.Printf(" размер буфера по умолчанию: %d байт\n", bw.Size())
low = &countWriter{}
bw = bufio.NewWriter(low)
bw.Write(make([]byte, 10_000))
fmt.Printf(" один Write на 10000 байт в пустой буфер: Write внизу ×%d %v\n", low.calls, low.sizes)
full, err := os.OpenFile("/dev/full", os.O_WRONLY, 0)
if err == nil {
bw = bufio.NewWriter(full)
ok := 0
for i := 0; i < 100; i++ {
if _, err := bw.Write(rec); err == nil {
ok++
}
}
fmt.Println(" bufio.Writer поверх /dev/full (запись всегда ENOSPC):")
fmt.Printf(" 100 x Write(10 байт) без ошибки: %d из 100\n", ok)
fmt.Printf(" Flush -> %s\n", errStr(bw.Flush()))
_, err = bw.Write(rec)
fmt.Printf(" Write после ошибки -> %s\n", errStr(err))
_, err = full.Write(rec)
fmt.Printf(" без bufio, первый же f.Write -> %s\n", errStr(err))
full.Close()
}
// ------------------------------------------------------------------ 8
show("8) CLOSE ТОЖЕ ПИШЕТ — И ЕГО ОШИБКУ ТЕРЯЮТ")
rp := filepath.Join(tmpDir, "report.txt")
err = writeReportDeferOnly(rp, 100)
st, _ := os.Stat(rp)
fmt.Printf(" defer f.Close(), bufio без Flush, 100 строк: вернула %s, размер файла %d байт\n",
errStr(err), st.Size())
var gz bytes.Buffer
zw := gzip.NewWriter(&gz)
zw.Write(bytes.Repeat([]byte("строка отчёта\n"), 100))
fmt.Printf(" gzip без Close: в нижнем писателе байт: %d\n", gz.Len())
_, err = readGzip(gz.Bytes())
fmt.Printf(" чтение назад -> %s\n", errStr(err))
zw.Flush()
fmt.Printf(" gzip после Flush, без Close: в нижнем писателе байт: %d\n", gz.Len())
_, err = readGzip(gz.Bytes())
fmt.Printf(" чтение назад -> %s\n", errStr(err))
zw.Close()
fmt.Printf(" gzip после Close: в нижнем писателе байт: %d\n", gz.Len())
out, err := readGzip(gz.Bytes())
fmt.Printf(" чтение назад -> %d байт, err=%s\n", len(out), errStr(err))
f, _ = os.Create(filepath.Join(tmpDir, "twice.txt"))
fmt.Printf(" первый f.Close() -> %s\n", errStr(f.Close()))
fmt.Printf(" второй f.Close() -> %s\n", errStr(f.Close()))
_, err = f.Write(rec)
fmt.Printf(" f.Write после Close -> %s\n", errStr(err))
fmt.Println(" запись в /dev/full:")
fmt.Printf(" defer w.Flush(); defer f.Close() -> функция вернула %s\n", errStr(writeToDeferFlush("/dev/full")))
fmt.Printf(" Flush проверен, Close в defer с err -> функция вернула %s\n", errStr(writeToChecked("/dev/full")))
// ------------------------------------------------------------------ 9
show("9) WRITE: ПОЛНАЯ ЗАПИСЬ ВМЕСТЕ С ОШИБКОЙ")
fmt.Println(" писатель возвращает len(p) и errQuota — контракт Write это допускает:")
nn, err = io.Copy(fullErrWriter{}, &onlyReader{r: strings.NewReader(text)})
fmt.Printf(" io.Copy (общий цикл) -> %d, err=%s\n", nn, errStr(err))
nn, err = io.Copy(fullErrWriter{}, strings.NewReader(text))
fmt.Printf(" io.Copy (*strings.Reader) -> %d, err=%s\n", nn, errStr(err))
bw = bufio.NewWriter(fullErrWriter{})
bw.WriteString(text)
fmt.Printf(" bufio: Flush -> %s\n", errStr(bw.Flush()))
fmt.Println(" писатель возвращает len(p)/2 и nil — контракт нарушен:")
nn, err = io.Copy(shortWriter{}, &onlyReader{r: strings.NewReader(text)})
fmt.Printf(" io.Copy (общий цикл) -> %d, err=%s\n", nn, errStr(err))
// ----------------------------------------------------------------- 10
show("10) ДАННЫЕ К ЗАДАЧАМ: РАЗМЕР ОТЧЁТА И ИТОГ IO.READALL")
total := 0
for i := 0; i < 100; i++ {
total += len(reportLine(i))
}
fmt.Printf(" 100 строк отчёта из раздела 8: %d байт; буфер bufio по умолчанию: %d байт\n",
total, bufio.NewWriter(io.Discard).Size())
all, _ := io.ReadAll(bytes.NewReader(mb))
fmt.Printf(" io.ReadAll(1048576 байт): len=%d, cap=%d — итоговый срез\n", len(all), cap(all))
fmt.Println(" Всего выделено за вызов (раздел 6) больше: промежуточные массивы")
fmt.Println(" роста становятся мусором, и их забирает сборщик.")
}
// errQuota — ошибка, которую писатель возвращает вместе с полной записью.
var errQuota = errors.New("quota exceeded")
// fullErrWriter принимает все байты и всё же сообщает об ошибке: так вправе
// сделать, например, писатель, который записал данные и обнаружил, что
// дальше места нет.
type fullErrWriter struct{}
func (fullErrWriter) Write(p []byte) (int, error) { return len(p), errQuota }
func readGzip(b []byte) ([]byte, error) {
zr, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
return io.ReadAll(zr)
}