Содержание урока · 13 разделов
- Чтение файлов в Go: от вызова к контракту
- io.Reader и io.Writer: два метода, на которых стоит весь ввод-вывод
- os.ReadFile и os.WriteFile: файл целиком одним вызовом
- os.Open, os.Create и defer Close: ручка на файл
- bufio.Scanner: построчное чтение без ручных буферов
- bufio.Writer: буферизация и Flush — зачем дописывать руками
- Размер файла, os.Stat и выбор инструмента
- Шпаргалка: файлы и потоки из этого урока
- Правила гигиены ввода-вывода
- Что дальше: из байтов в данные
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Чтение файлов в Go: от вызова к контракту#
Пора вывести программу за пределы собственной памяти. До сих пор каждый пример курса начинался и заканчивался в оперативной памяти: срезы, карты, каналы — всё это исчезало вместе с процессом. Файл — первый внешний мир, с которым программа говорит напрямую, и чтение файлов в Go устроено так, что вы уже знаете половину языка, на котором этот разговор идёт. Запрос «чтение файлов go» на самом деле распадается на три навыка: выполнить один вызов целиком (os.ReadFile), работать потоком через контракт io.Reader и ускорить оба пути буферизацией bufio. Этим трём навыкам и посвящён урок.
Начнём с карты местности. В стандартной библиотеке за ввод-вывод отвечает связка пакетов: os даёт доступ к файловой системе — открыть, создать, прочитать целиком, записать целиком, спросить размер; io описывает два главных контракта — Reader и Writer — и сигналы вроде io.EOF; bufio оборачивает любой поток буфером, чтобы не дёргать диск (или сеть) на каждый байт. Эти три слоя лежат друг на друге: os.File умеет и Reader, и Writer, bufio принимает любого Reader и отдаёт тоже Reader, а функция вроде fmt.Fprintf согласна писать в ANY Writer — в файл, в сеть, в буфер в памяти. Поэтому «прочитать файл» и «прочитать HTTP-ответ» — это один и тот же цикл с разными источниками.
Договоримся о честности песочницы заранее, как в прошлом уроке. Резервный движок знает только fmt, strings, strconv, math, sort, errors и time — пакет os, bufio и io он не исполняет вовсе, и мы зафиксируем точные тексты отказов. Основной движок (yaegi) исполняет настоящие файловые операции: os.WriteFile, os.ReadFile, bufio.Scanner — файлы реально создаются в /tmp, и вывод раннеров детерминирован. А там, где файлы недоступны, мы построим Reader своими руками из строки — чистый Go без единого импорта из «файловой» тройки: контракт io.Reader — это несколько строк кода, и никто не мешает реализовать его на строке. Такой ручной Reader исполняется обоими движками и станет нашим полигоном.
io.Reader и io.Writer: два метода, на которых стоит весь ввод-вывод#
Вспомним контракт из урока про интерфейсы — теперь он понадобится ежедневно. io.Reader — это один метод: Read(p []byte) (n int, err error). Смысл подписи: потребитель вызывает Read и ПЕРЕДАЁТ свой буфер p — «вот память, заполняй»; Read кладёт туда сколько смог и возвращает n — сколько байт реально записал; err — ошибка, и её особая форма io.EOF означает «данные кончились, это не сбой». Ключевые правила контракта: n может быть меньше len(p) — и это норма, а не ошибка; буфер отдаёт потребитель, поэтому размер порции выбирает тот, кто читает; io.EOF возвращается только вместе с n = 0 — «прочитал что-то и заодно EOF» контракт запрещает. io.Writer — зеркало: Write(p []byte) (n int, err error) — «вот байты, возьми сколько сможешь и скажи».
Отсюда канонический цикл чтения, который вы будете писать всю карьеру: объявить буфер, вызывать Read в цикле, прибавлять n к счётчику, при err != nil — выйти, причём io.EOF — это выход ШТАТНЫЙ, а всё остальное — по-настоящему ошибка. Именно этот цикл мы собирали в десятом уроке поверх strings.NewReader; сегодня источник станет файлом, но цикл не изменится ни на букву — в этом и смысл контракта. Файл, сетевое соединение, HTTP-тело, gz-архив, криптографический поток — всё говорит на одной сигнатуре Read, и функция, написанная против io.Reader, работает со всеми источниками сразу.
Почему нельзя «просто прочитать всё сразу»? Можно — os.ReadFile так и делает, — но у потока может не быть конца или его размер может быть гигабайтом: сетевое соединение бесконечно, конвейер (pipe) отдаёт байты по мере появления. Контракт Read придуман именно для таких источников: вы никогда не просите больше, чем ваш буфер, и никогда не держите в памяти больше одной порции. А буфер — это срез байт, и вот где байтовое мышление прошлого урока включается на полную: make([]byte, 4096) — это заголовок на 24 байта плюс массив на четыре килобайта, и передача его в Read не копирует массив — Read заполняет ВАШУ память, а не свою.
Ручной Reader на строке: контракт своими руками
Раннер двойной: этот код исполняют ОБА движка — здесь нет ни одного импорта из файловой тройки, только errors и fmt. Запустите и сверьте: три порции по 4, затем 0 и EOF, итого 12 байт — len("привет") в байтах UTF-8, по два на каждую букву. Эксперимент: сделайте буфер make([]byte, 5) — порции станут 5, 5, 2, EOF; сделайте 12 — вся строка за один вызов и EOF вторым. Поменяйте строку на "hi" — один вызов по 2 байта. Обратите внимание: строка "привет" весит 12 байт, хотя букв шесть, — это то же байтовое знание из прошлого урока, теперь оно отвечает за содержимое файла.
Запустите код, чтобы увидеть вывод.
Проверьте себя
Источник исчерпан: вы вызываете Read ещё раз. Что контракт io.Reader предписывает вернуть?
os.ReadFile и os.WriteFile: файл целиком одним вызовом#
Когда файл небольшой и вам нужны все его байты сразу, ручной цикл не нужен — стандартная библиотека даёт две функции-однострочника. os.ReadFile(path) открывает файл, читает его целиком в память и возвращает пару (data []byte, err error); os.WriteFile(path, data, perm) создаёт (или усекает!) файл и пишет байты целиком, perm — права доступа, обычно 0644: владелец может всё, остальные — только читать. Это «простой путь» урока: два вызова закрывают девять из десяти бытовых задач — прочитать конфиг, сохранить отчёт, перегнать небольшой файл в строку string(data) или обратно []byte(s).
Три детали, на которых спотыкаются. Первая: ReadFile возвращает []byte, а не string — файл с точки зрения Go это БАЙТЫ, и только вы решаете, интерпретировать ли их как текст UTF-8 через string(data); конверсия копирует содержимое, у файла в гигабайт это второй гигабайт памяти. Вторая: os.WriteFile ПЕРЕЗАПИСЫВАЕТ файл с нуля — если там было длинное содержимое, а вы пишете короткое, хвост не «допишется», а исчезнет: файл усекается до нуля перед записью. Дописать в конец (append) через WriteFile нельзя — для этого открывают файл через os.OpenFile с флагом os.O_APPEND, о чём мы поговорим в блоке про os.Open. Третья: err надо проверять на ОБА вызова — ReadFile честно вернёт пустой срез и ошибку, если файла нет, и ошибка эта — обычное значение, которое функция возвращает парой с данными, как мы учились в уроке про обёртывание ошибок.
Где проходит граница применимости простого пути? В памяти. os.ReadFile держит весь файл в памяти разом: лог на сто мегабайт — сто мегабайт, лог на десять гигабайт — уже проблема, и не только памяти: пока файл читается целиком, вы не обработали НИ ОДНОЙ строки, хотя они были готовы с самого начала. Для больших файлов и потоков без конца нужен цикл Read или bufio — об этом ниже. Правило выбора простое: знаете размер и он помещается в память — ReadFile; не знаете или не помещается — поток.
Запись и чтение файла целиком
Раннер нативный: файловые операции исполняет только основной движок — резервный откажет с текстом «пакет \"os\" недоступен в песочнице», файлы здесь создаются по-настоящему, в /tmp. Запустите: обе ошибки — nil, в файле 42 байта (по два на каждую кириллическую букву, плюс пробелы и переводы строк), содержимое печатается как две строки. Эксперимент: допишите третий вызов os.WriteFile с []byte("коротко") и снова прочитайте — файл стал пятибайтовым: усечение перед записью стёрло старое содержимое. Уберите второй аргумент прав 0644 — код не соберётся: perm обязателен.
Запустите код, чтобы увидеть вывод.
Что напечатает цикл чтения?
Прочитайте код и выберите, что он выведет:
package main
import (
"errors"
"fmt"
)
// box — Reader над строкой: тот же контракт, что у файлов.
type box struct {
data string
pos int
}
func (b *box) Read(p []byte) (int, error) {
if b.pos >= len(b.data) {
return 0, errors.New("EOF")
}
n := 0
for b.pos < len(b.data) && n < len(p) {
p[n] = b.data[b.pos]
b.pos++
n++
}
return n, nil
}
func main() {
r := &box{data: "привет"}
buf := make([]byte, 5)
for {
n, err := r.Read(buf)
fmt.Println(n, err)
if err != nil {
break
}
}
}os.Open, os.Create и defer Close: ручка на файл#
os.ReadFile хорош, пока файл целиком помещается в память. Как только вы хотите читать по кусочку, писать по кусочку или дописывать в конец — нужна ручка на файл: os.File. Две функции её выдают: os.Open(path) открывает файл ТОЛЬКО на чтение, os.Create(path) создаёт файл или усекает существующий в ноль и открывает на запись (права 0666 — уточняются маской umask процесса). Обе возвращают пару `(os.File, error)`, и первая проверка после вызова — err != nil: файла может не существовать, прав может не хватить, путь может оказаться каталогом. Тонкость, которую стоит запомнить: *os.File — это не «файл на диске», а дескриптор открытого файла в вашей программе; сам файл лежит на месте, ручка — в памяти, и её, как всякий ресурс операционной системы, надо закрывать.
Закрытие в Go делают идиомой defer f.Close() сразу после успешного открытия: отложенный вызов сработает при выходе из функции — хоть по return, хоть по панике — и дескриптор вернётся системе. Почему это важно: число открытых дескрипторов на процесс ограничено, программа, теряющая ручки в цикле, упадёт с «too many open files» на тысячах итераций; к тому же незакрытый файл на запись может потерять последние байты, если ОС держала их в своих буферах. А вот обратное заблуждение опаснее: Close — это НЕ запись данных. Данные, застрявшие в буфере bufio.Writer, при Close не дописываются — сначала Flush, потом Close, и к этому мы вернёмся в блоке про буферизацию.
Третий член семейства — os.OpenFile(path, flag, perm) — общий случай: флаги os.O_RDONLY, os.O_WRONLY, os.O_RDWR, os.O_APPEND, os.O_CREATE комбинируются через вертикальную черту, и так делается, например, дозапись в лог: os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644). В уроке нам хватит Open и Create, но знать про OpenFile стоит хотя бы для того, чтобы читать чужой код: это он, а не WriteFile, обеспечивает append-семантику.
bufio.Scanner: построчное чтение без ручных буферов#
Чтение текстового файла построчно — самая частая операция над файлами вообще, и писать её через голый Read с ручным поиском переводов строки никто не хочет. Для этого в пакете bufio живёт Scanner: sc := bufio.NewScanner(f) принимает ЛЮБОГО Reader (файл — Reader, мы это уже знаем) и разбивает поток на «токены» — по умолчанию строки. Цикл выглядит как заклинание: for sc.Scan() { fmt.Println(sc.Text()) } — Scan подготавливает следующую строку и возвращает true, пока строки есть; Text() отдаёт её без завершающего перевода строки. У Scanner есть и Bytes() — те же байты без копирования в строку, — и Err() — ошибка, если цикл кончился НЕ из-за конца данных: Scan возвращает false и на EOF, и на сбое, различать их обязаны вы, вызывая Err после цикла.
Что здесь такого волшебного? Буферизация. Scanner читает из файла не по байту, а большими кусками (стартовый буфер — 4 килобайта, растёт до 64К), ищет '\n' в куске и отдаёт строки из своего внутреннего буфера — диск дёргается в тысячи раз реже. Это та же экономия, что у шин на материнской плате из прошлого урока: за один дорогой ход забираем много дешёвых байт. Но у буфера есть потолок: по умолчанию одна строка не может быть длиннее 64 килобайт (bufio.MaxScanTokenSize), и Scan молча вернёт false с ошибкой «token too long». Логи JSON-строкой на мегабайт Scanner проглотить не сможет, пока вы не увеличите потолок через sc.Buffer(make([]byte, 0, 10241024), 10241024) — известная грабля, о которой честно предупреждает документация.
Scanner — читатель, писателем служит bufio.Writer, но перед ним мы прогоним Scanner по настоящему файлу: откроем через os.Open, поставим defer Close и пройдём циклом. Обратите внимание на типы в цепочке: os.Open вернула *os.File, NewScanner приняла её как io.Reader — файл не знал, что его обернули, а Scanner не знает, что внутри файл: контракт в действии.
Построчное чтение через Scanner
Раннер нативный: bufio и os в резервном движке недоступны — запускайте на основном. Запустите: три строки по счётчику, ошибка сканера — nil (цикл кончился ШТАТНЫМ концом данных). Эксперимент: добавьте в конец файла четвёртую строку с завершающим \n — вывод не изменится, потому что Text() отдаёт строку без перевода строки; сотрите последний символ \n у «гаммы» — тоже ничего: последняя строка без перевода строки всё равно валидна. Поменяйте источник: NewScanner примет любую ручку, которая умеет Read, — файл, сетевое соединение, строку через strings.NewReader; контракт делает их взаимозаменяемыми, и Scanner не заметит подмены.
Запустите код, чтобы увидеть вывод.
bufio.Writer: буферизация и Flush — зачем дописывать руками#
Чтение мы ускорили буфером, теперь запись. bufio.NewWriter(f) оборачивает любого Writer в буфер на 4096 байт по умолчанию: все Write и fmt.Fprintf кладут байты НЕ в файл, а в этот буфер в памяти; диск получает данные, когда буфер заполнился, — по 4 килобайта за ход. Экономия та же, что у Scanner, в обратную сторону: тысячи мелких записей складываются в несколько больших. Записываете по одному числу в цикле из десяти тысяч итераций? Без буфера — десять тысяч системных вызовов; с буфером — три. Это самый дешёвый прирост производительности в вводе-выводе, доступный одной строкой кода.
Но у буфера есть обратная сторона, и она — главная ловушка урока: байты в буфере — это ещё НЕ файл. Пока буфер не полон, файл про них не знает. Забыли дописать — данные потеряны при выходе из программы. Метод Flush() выталкивает всё накопленное в настоящий Writer — его вызывают перед завершением записи ВСЕГДА; правильная последовательность на выходе: Flush, потом Close. Проверить, сколько байт ещё сидит в буфере, можно через w.Buffered(), а убедиться, что всё вытолкнуто, — через w.Flush() и проверку его ошибки: Flush тоже возвращает err, и молча проглотить его — значит не заметить сбой записи на самом ответственном шаге.
Тонкость с размерами: bufio.NewWriterSize(f, 1024) делает буфер нужного размера, а если одиночная запись БОЛЬШЕ буфера, Writer не глотает её частями, а пишет напрямую мимо буфера — в файл. Поэтому буферизация ускоряет именно много мелких записей и почти не влияет на несколько больших. И держите в голове asymmetry: у Scanner ошибка достаётся через Err() ПОСЛЕ цикла, у Writer — из каждого вызова Write и из Flush; у обоих ошибка НЕ возвращается там, где вы интуитивно ждёте её увидеть.
Буфер: до Flush и после
Раннер нативный: запускайте на основном движке — файлы здесь настоящие. Запустите: до Flush на диске 0 байт, хотя две строки уже «записаны»; после Flush — обе строки на месте. Это живая демонстрация ловушки урока: буфер — память, Flush — диск. Эксперимент: закомментируйте w.Flush() и перезапустите — файл останется пустым навсегда. Добавьте fmt.Println("в буфере байт:", w.Buffered()) после второй записи — там 52: каждая из двух строк весит 26 байт, кириллица по два байта на букву, плюс пробел и перевод строки.
Запустите код, чтобы увидеть вывод.
Проверьте себя
Программа записала три строки через bufio.Writer и завершилась, не вызвав Flush. Что окажется на диске?
Размер файла, os.Stat и выбор инструмента#
Прежде чем читать файл, полезно знать, СКОЛЬКО его. os.Stat(path) возвращает структуру FileInfo с методами Size() (размер в байтах), IsDir(), ModTime() и именем; для несуществующего пути Stat вернёт ошибку — так в Go проверяют существование файла. Size помогает принять решение о стратегии: файл на сотню килобайт — os.ReadFile и не думать, файл на гигабайт — только потоки, файл в сети — размер заранее неизвестен вовсе, значит ReadFile по определению не подходит. Второе применение Stat — метаданные: время модификации для кешей, признак каталога при обходе дерева через os.ReadDir.
Соберём полную лестницу инструментов урока в один список выбора. Нужно все байты сразу и файл небольшой — os.ReadFile. Нужно записать всё сразу — os.WriteFile с правами 0644. Нужно читать поток по строкам — os.Open плюс bufio.Scanner (не забыть Err после цикла). Нужно писать много мелких кусочков — os.Create плюс bufio.NewWriter, в конце Flush и Close. Нужно дописывать в существующий файл — os.OpenFile с os.O_APPEND. Нужен сам размер или существование — os.Stat. Каждая ступенька стоит одной проверки ошибки дороже предыдущей, и именно эта лестница, а не отдельные функции, и есть ответ на запрос «чтение файлов go»: язык даёт вам и однострочник, и полный контроль, выбор — за размером задачи.
И последнее — про обработку ошибок, без которой ни одна ступенька не работает. Ошибки файлов говорят сами за себя: прочтите их текст — «open /tmp/x.txt: no such file or directory» содержит операцию, путь и причину; вам остаётся решить, что делать: завершиться с сообщением пользователю, пропустить файл в цикле или попробовать другой путь. Оборачивать ошибку контекстом полезно: fmt.Errorf("читаю конфиг %s: %w", path, err) сохранит причину для errors.Is и добавит, ЧТО вы делали — через полгода это спасёт час отладки. Единственная ошибка, которую НЕ надо считать провалом, — io.EOF из цикла чтения: это штатный сигнал конца, и смешивать её с настоящими сбоями — самая частая ошибка новичка в этом жанре.
Резервный движок знает только fmt, strings, strconv, math, sort, errors и time — файловой тройки в нём нет: import "os" даёт отказ «пакет "os" недоступен в песочнице (доступны: fmt, strings, strconv, math, sort, errors, time)», точно такой же текст — для "bufio" и "io". Более того, даже strings.NewReader там нет: «у пакета strings нет члена NewReader» — поэтому Reader-полигон урока собран руками из строки, и он исполняется обоими движками. Основной движок (yaegi) исполняет настоящие файловые операции: os.WriteFile, os.ReadFile, os.Open, os.Create, bufio.Scanner, bufio.Writer с Flush — файлы реально создаются и читаются в /tmp, раннеры урока нативны и детерминированы (мы прогнали каждый трижды). Запуск runner-ов с файлами на резервном движке бесполезен — переключитесь на основной.
Допишите цикл чтения до EOF
Восстановите вручную канонический цикл: размер буфера и оператор выхода из цикла при ошибке. Ответы введите маленькими буквами, без пробелов.
Соберите Reader и одно чтение
Соберите программу: тип-Reader над строкой, затем в main одно чтение в буфер и печать «сколько взяли, что взяли, какая ошибка». Две строки импорта — по одной на пакет.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шпаргалка: файлы и потоки из этого урока#
| Инструмент | Что делает | Как выглядит | Осторожно |
|---|---|---|---|
| os.ReadFile | файл целиком в память, []byte | data, err := os.ReadFile(path) | гигабайтный файл — гигабайт памяти |
| os.WriteFile | записать []byte целиком | os.WriteFile(path, data, 0644) | усекает файл: старое содержимое стирается |
| os.Open | открыть на чтение, *os.File | f, err := os.Open(path) | сразу defer f.Close() после проверки err |
| os.Create | создать/усечь на запись | f, err := os.Create(path) | права 0666, существующий файл обнуляется |
| os.OpenFile | открыть с флагами | os.OpenFile(p, os.O_APPEND|os.O_CREATE, 0644) | это единственный честный append |
| os.Stat | размер и метаданные | st, err := os.Stat(path); st.Size() | ошибка = файла нет; проверка существования |
| io.Reader | контракт чтения потока | Read(p []byte) (n int, err error) | буфер даёт потребитель; n < len(p) — норма |
| io.Writer | контракт записи потока | Write(p []byte) (n int, err error) | n может быть меньше len(p) — проверяйте |
| io.EOF | штатный конец потока | if err == io.EOF { break } | не сбой; с n > 0 не приходит |
| bufio.Scanner | построчное чтение | for sc.Scan() { sc.Text() } | строка длиннее 64К — token too long; Err() после цикла |
| bufio.Writer | буферизованная запись | w := bufio.NewWriter(f) … w.Flush() | без Flush данные теряются; запись больше буфера идёт напрямую |
| defer f.Close() | гарантированное закрытие | defer f.Close() | Close не пишет данные: сначала Flush |
Правила гигиены ввода-вывода#
- Размер решает всё: файл, который помещается в память, — os.ReadFile; поток неизвестного или огромного размера — только io.Reader-цикл или bufio.
- Проверяйте err у КАЖДОГО вызова: ReadFile, WriteFile, Open, Create, Flush — каждая возвращает ошибку парой с результатом, и молча проглоченная ошибка превращается в пустой файл.
- defer f.Close() ставьте сразу после успешного os.Open/os.Create: дескрипторы — ограниченный ресурс, утечка проявляется как «too many open files» на тысячной итерации.
- Порядок завершения записи: w.Flush() с проверкой ошибки, потом f.Close() — Close не вытолкивает чужой буфер.
- io.EOF — не ошибка, а сигнал конца: в цикле чтения выходите по ней спокойно, наверх передавайте всё остальное.
- Для строк — bufio.Scanner (и не забудьте sc.Err() после цикла: false бывает и от сбоя), для байтов — Read с буфером, размер которого выбираете вы.
- os.WriteFile и os.Create усекают файл до нуля: дописывать в конец умеет только os.OpenFile с os.O_APPEND.
- Права 0644 — осмысленный дефолт для данных: запись владельцу, чтение всем; 0777 на файл — почти всегда баг.
- У длинных строк в Scanner есть потолок 64К (bufio.MaxScanTokenSize): «token too long» лечится sc.Buffer, а не отказом от Scanner.
- Буфер ускоряет МНОГО мелких записей: одна большая запись проскочит мимо буфера напрямую в файл — и это нормально.
Что дальше: из байтов в данные#
Соберём урок в одну карту. Контракты: io.Reader — Read(p []byte) (n int, err error) с буфером от потребителя и io.EOF как штатным концом, io.Writer — Write с честным n; любой файл, сеть и буфер говорят на этой паре сигнатур. Простой путь: os.ReadFile и os.WriteFile — файл целиком, но память платит за всё, а WriteFile усекает. Ручки: os.Open, os.Create, defer Close сразу после проверки ошибки, os.OpenFile для append. Буферизация: bufio.Scanner для строк (Scan/Text/Err, потолок 64К), bufio.Writer для записи (4096 по умолчанию, Flush обязателен, запись больше буфера идёт напрямую). Метаданные: os.Stat и Size для выбора стратегии. Мост из прошлого урока работает на каждом шаге: буфер — это срез с заголовком на 24 байта, строка — байты UTF-8, len считает именно их, а Reader на строке — двадцать строк чистого Go.
Двадцать первый урок — JSON: кодирование и декодирование. Файлы дали нам байты, но байты без формы — просто содержимое; encoding/json научит складывать структуры в текст и доставать их обратно: struct tags, Marshal, Unmarshal, вложенные структуры. Связка прямая: прочитали конфиг os.ReadFile — развернули байты в структуру Unmarshal'ом; собрали данные — Marshal и os.WriteFile. Байтовое мышление этого урока останется с вами: JSON — это те же байты UTF-8, просто с договорённостью о форме.
До встречи на странице курса. Если захочется перечитать основу контрактов — урок про интерфейсы: io.Reader/Writer мы разбирали именно там, а сегодня файлы превратили теорию в инструмент. Если захочется вернуться к байтам и заголовкам — урок про unsafe и память: буфер, который вы передаёте в Read, — это тот самый срез с заголовком на 24 байта, чей вес мы измеряли Sizeof'ом.
Карточки урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Ввод-вывод в Go стоит на двух контрактах: io.Reader — Read(p []byte) (n int, err error), io.Writer — Write(p []byte) (n int, err error); буфер отдаёт потребитель, n честно показывает, сколько байт обработано.
- io.EOF — штатный сигнал конца потока, а не ошибка: канонический цикл «Read → использовать n → при err != nil выйти», где EOF — спокойный выход, всё остальное — передача наверх.
- os.ReadFile и os.WriteFile — файл целиком одним вызовом: data, err := os.ReadFile(path); os.WriteFile(path, data, 0644); WriteFile усекает файл, дописывание — только os.OpenFile с os.O_APPEND.
- os.Open и os.Create дают ручку *os.File — дескриптор ограниченного ресурса: defer f.Close() сразу после проверки ошибки, иначе «too many open files» на тысячах итераций.
- Close не записывает данные: перед закрытием bufio.Writer обязателен Flush с проверкой ошибки — иначе буфер умирает вместе с программой.
- bufio.Scanner — построчное чтение: for sc.Scan() { sc.Text() }, буферизация кусками от 4К, потолок 64К на строку, sc.Err() после цикла различает конец и сбой.
- bufio.Writer — буферизованная запись: 4096 байт по умолчанию, запись больше буфера идёт напрямую в файл, ускоряет именно много мелких записей.
- os.Stat даёт размер и метаданные — по ним выбирают стратегию: небольшой файл — ReadFile, гигабайт или поток — только цикл Read/Scanner.
- Файл — это байты: ReadFile возвращает []byte, string(data) — отдельная конверсия с копированием; len считает байты UTF-8, «привет» — 12 байт.
- Песочница честна: фолбэк не знает os, bufio, io (и даже strings.NewReader), нативный движок исполняет настоящие файловые операции в /tmp; ручной Reader из двадцати строк чистого Go исполняют оба.
Домашнее задание
Часть первая — ручной Reader: наберите sReader из раннера и прогоните строку "привет" с буферами 3, 4, 5 и 12; выпишите порции для каждого размера и объясните письменно, почему содержимое всегда одно, а нарезка разная. Часть вторая — файл целиком: напишите программу, которая os.WriteFile'ом создаёт файл с тремя вашими любимыми строками, os.ReadFile'ом читает его и печатает len(data) рядом с содержимым; затем запишите в тот же файл одно короткое слово и объясните в комментарии, почему len уменьшился. Часть третья — Scanner: создайте файл на пять строк (последняя без перевода строки), прочитайте его os.Open плюс bufio.Scanner с нумерацией, после цикла напечатайте sc.Err(); затем добавьте строку длиной в сто тысяч символов, получите «token too long» и вылечите его через sc.Buffer — размер потолка подберите сами. Часть четвёртая — буфер: os.Create, bufio.NewWriterSize(f, 32), запишите в цикле двадцать коротких строк, ПОСЛЕ цикла напечатайте w.Buffered(), прочитайте файл до Flush (объясните ноль), выполните Flush и прочитайте снова; в комментарии опишите порядок «Flush → Close» и что случится, если его нарушить. Комментарии в коде — по-русски.
Личные заметкиПусто
Вопросы и ответы по уроку
Когда брать os.ReadFile, а когда os.Open с bufio?
Ориентир — размер и форма обработки. Файл целиком помещается в память и нужен весь сразу (конфиг, шаблон, небольшой JSON) — os.ReadFile: один вызов, минимум кода. Файл большой (сотни мегабайт и больше), поток без конца (сеть, pipe) или обрабатывать надо построчно на ходу — os.Open плюс bufio.Scanner для строк или цикл Read для байтов: память тратится на одну порцию, а не на весь файл. Середина — вопрос вкуса и профиля, но правило «знаю размер и он скромный — ReadFile» спасает от девяноста процентов проблем.
Почему данные не записались, хотя Write прошёл без ошибки?
Почти наверняка вы пишете через bufio.Writer и забыли Flush. Байты лежали в буфере в памяти (по умолчанию 4096), файл про них не знал, и при завершении программы буфер исчез вместе с содержимым. Дисциплина: w.Flush() с проверкой ошибки, затем f.Close(). Учтите, что Close закрывает дескриптор, но НЕ вытолкивает чужой bufio-буфер, а записи, превышающие размер буфера, bufio пропускает напрямую в файл — поэтому иногда «забытый Flush» маскируется частично записанными данными.
Что такое io.EOF и надо ли его обрабатывать как ошибку?
io.EOF — предобъявленная в пакете io ошибка-сигнал: «данных больше нет». Это штатный конец потока, а не сбой: в цикле чтения по err == io.EOF выходят спокойно, без логов и ретраев. Настоящие ошибки (нет файла, нет прав, обрыв сети) передаются наверх или обрабатываются по месту. Отдельное правило контракта: io.EOF приходит только с n = 0 — если Read вернул данные, ошибки при этом быть не может, поэтому сначала используйте n, потом проверяйте err.
Работают ли файлы в песочнице самоучителя?
Частично, и это зафиксировано честно. Резервный движок знает только fmt, strings, strconv, math, sort, errors и time: import "os" даёт отказ «пакет "os" недоступен в песочнице (доступны: fmt, strings, strconv, math, sort, errors, time)», то же — для "bufio" и "io"; даже strings.NewReader там отсутствует («у пакета strings нет члена NewReader»). Основной движок (yaegi) исполняет настоящие файловые операции: os.WriteFile, os.ReadFile, os.Open, os.Create, bufio.Scanner, bufio.Writer с Flush — файлы реально создаются и читаются в /tmp. Поэтому файловые раннеры урока нативные, а Reader-полигон собран руками из строки и исполняется обоими движками.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Fiber: экспресс-подобный веб-фреймворк на Fasthttp
Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.
- Поднимать HTTP-сервер на Fiber за минуты
- Проектировать маршруты, группы и версии API
- Писать middleware: логи, CORS, лимиты, recovery
- Принимать и отдавать JSON с валидацией