Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 20 из 25 ~22 мин +55 XP

Файлы и ввод-вывод

«Чтение файлов go» — запрос, с которого начинается практика: до этого урока все программы курса жили в памяти и печатали в консоль, а теперь мы выйдем наружу — к диску, потокам и файлам. Двадцатый урок курса собирает в одну картину os.ReadFile и os.WriteFile — простой путь «файл целиком за один вызов», базовые os.Open и os.Create с defer Close, контракты io.Reader и io.Writer из десятого урока, которые становятся вдруг не теорией, а ежедневным инструментом, и пакет bufio: «bufio golang» — это про буферизацию, Scanner для построчного чтения и Writer, который копит байты и отдаёт их диску порциями. Мост из прошлого урока лежит на виду: заголовки срезов — это готовые буферы, строки из байтов — это содержимое файлов, а EOF — не катастрофа, а штатный сигнал конца потока.

Содержание урока · 13 разделов

Чтение файлов в 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 из цикла чтения: это штатный сигнал конца, и смешивать её с настоящими сбоями — самая частая ошибка новичка в этом жанре.

Допишите цикл чтения до EOF

Восстановите вручную канонический цикл: размер буфера и оператор выхода из цикла при ошибке. Ответы введите маленькими буквами, без пробелов.

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, ) for { n, err := r.Read(buf) fmt.Println(n, err) if err != nil { } } }

Соберите Reader и одно чтение

Соберите программу: тип-Reader над строкой, затем в main одно чтение в буфер и печать «сколько взяли, что взяли, какая ошибка». Две строки импорта — по одной на пакет.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Шпаргалка: файлы и потоки из этого урока#

ИнструментЧто делаетКак выглядитОсторожно
os.ReadFileфайл целиком в память, []bytedata, err := os.ReadFile(path)гигабайтный файл — гигабайт памяти
os.WriteFileзаписать []byte целикомos.WriteFile(path, data, 0644)усекает файл: старое содержимое стирается
os.Openоткрыть на чтение, *os.Filef, 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'ом.

Карточки урока

Термин

Контракт io.Reader

нажмите, чтобы перевернуть
Определение

Один метод: Read(p []byte) (n int, err error). Буфер p даёт ПОТРЕБИТЕЛЬ — вы решаете, какими порциями читать. n — сколько байт реально легло (может быть меньше len(p) — это норма). err — ошибка; io.EOF означает штатный конец данных и приходит только с n = 0. Канонический цикл: Read в for, использовать n, выход при err != nil, io.EOF — спокойный выход, остальное — наверх. Источники-строки в тестах — strings.NewReader; файл, сеть, HTTP-тело говорят на той же сигнатуре.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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» и что случится, если его нарушить. Комментарии в коде — по-русски.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · Файлы и ввод-вывод)

Вопросы и ответы по уроку

Когда брать 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-полигон собран руками из строки и исполняется обоими движками.

Термины урока

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

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией