Описание типов пакета io языка программирования GO
type ByteReader
type ByteReader interface {
ReadByte() (byte, error)
}
ByteReader — это интерфейс, который оборачивает метод ReadByte.
ReadByte считывает и возвращает следующий байт из ввода или любую возникшую ошибку. Если ReadByte возвращает ошибку, входной байт не был обработан, и возвращаемое значение байта не определено.
ReadByte предоставляет эффективный интерфейс для обработки по одному байту за раз. Reader, который не реализует ByteReader, можно обернуть с помощью bufio.NewReader, чтобы добавить этот метод.
type ByteScanner
type ByteScanner interface {
ByteReader
UnreadByte() error
}
ByteScanner — это интерфейс, который добавляет метод UnreadByte к базовому методу ReadByte.
UnreadByte заставляет следующий вызов ReadByte возвращать последний прочитанный байт. Если последняя операция не была успешным вызовом ReadByte, UnreadByte может вернуть ошибку, не прочитать последний прочитанный байт (или байт, предшествующий последнему непрочитанному байту), или (в реализациях, поддерживающих интерфейс Seeker) перейти на один байт перед текущим смещением.
type ByteWriter
type ByteWriter interface {
WriteByte(c byte) error
}
ByteWriter — это интерфейс, который оборачивает метод WriteByte.
type Closer
type Closer interface {
Close() error
}
Closer — это интерфейс, который оборачивает базовый метод Close.
Поведение Close после первого вызова не определено. Конкретные реализации могут документировать свое собственное поведение.
type LimitedReader
type LimitedReader struct {
R Reader // базовый читатель
N int64 // максимальное количество оставшихся байтов
}
LimitedReader читает из R, но ограничивает количество возвращаемых данных до N байтов. Каждый вызов Read обновляет N, чтобы отразить новое количество оставшихся данных. Read возвращает EOF, когда N <= 0 или когда базовый R возвращает EOF.
func (*LimitedReader) Read
func (l *LimitedReader) Read(p []byte) (n int, err error)
Пример
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// Создаем источник данных - строка длиной 100 символов
data := strings.Repeat("abcdefghij", 10) // 100 байт
reader := strings.NewReader(data)
// Создаем LimitedReader, который прочитает только первые 35 байт
limitedReader := &io.LimitedReader{
R: reader, // базовый reader
N: 35, // лимит в 35 байт
}
// Буфер для чтения
buf := make([]byte, 10) // читаем по 10 байт за раз
var totalRead int
for {
// Читаем данные
n, err := limitedReader.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("\nДостигнут лимит или конец данных")
} else {
fmt.Println("\nОшибка чтения:", err)
}
break
}
totalRead += n
fmt.Printf("Прочитано %d байт: %q\n", n, buf[:n])
fmt.Printf("Осталось прочитать: %d байт\n", limitedReader.N)
}
fmt.Println("Всего прочитано:", totalRead, "байт")
fmt.Println("Осталось данных в основном reader:", reader.Len(), "байт")
}
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 25 байт
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 15 байт
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 5 байт
Прочитано 5 байт: "abcde"
Осталось прочитать: 0 байт
Достигнут лимит или конец данных
Всего прочитано: 35 байт
Осталось данных в основном reader: 65 байт
type OffsetWriter
type OffsetWriter struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Объяснение OffsetWriter
OffsetWriter
в Go — это обёртка вокруг WriterAt
, которая позволяет записывать данные, начиная с указанного смещения в базовом потоке. Он автоматически управляет позицией записи и предоставляет три ключевые функции: Write
(пишет с текущей позиции), WriteAt
(пишет по конкретному смещению без изменения позиции) и Seek
(меняет текущую позицию). Это особенно полезно для записи в определённые места файлов (например, при обновлении заголовков или работе с бинарными форматами), когда нужно контролировать точное расположение данных без ручного управления позицией в базовом WriterAt
.
Пример с использованием всех методов OffsetWriter
:
package main
import (
"fmt"
"io"
"os"
)
func main() {
// Создаем временный файл
tmpFile, err := os.CreateTemp("", "offset-writer-demo")
if err != nil {
panic(err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
// Заполняем файл пробельными символами для наглядности
_, err = tmpFile.WriteString("=== Начало файла ===\n" +
strings.Repeat(".", 100) + "\n=== Конец файла ===")
if err != nil {
panic(err)
}
// Создаем OffsetWriter с начальным смещением 20 байт
ow := io.NewOffsetWriter(tmpFile, 20)
// 1. Используем Write - пишет с текущего смещения
fmt.Println("\n1. Запись через Write()")
_, err = ow.Write([]byte("ПЕРВАЯ ЗАПИСЬ"))
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 2. Используем Seek для изменения позиции
fmt.Println("\n2. Seek(10, io.SeekCurrent)")
newPos, err := ow.Seek(10, io.SeekCurrent) // Перемещаемся на +10 байт от текущей позиции
if err != nil {
panic(err)
}
fmt.Printf("Новая позиция: %d\n", newPos)
// 3. Еще одна запись через Write
_, err = ow.Write([]byte("ВТОРАЯ ЗАПИСЬ"))
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 4. Используем WriteAt - пишет по абсолютному смещению (не меняет текущую позицию)
fmt.Println("\n4. WriteAt() по смещению 5")
_, err = ow.WriteAt([]byte("ТРЕТЬЯ"), 5)
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 5. Проверяем текущую позицию после WriteAt
pos, _ := ow.Seek(0, io.SeekCurrent)
fmt.Printf("\n5. Текущая позиция после WriteAt: %d\n", pos)
// 6. Возвращаемся в начало и пишем еще
fmt.Println("\n6. Seek(0, io.SeekStart) + Write()")
ow.Seek(0, io.SeekStart)
ow.Write([]byte("НАЧАЛО"))
printFileContent(tmpFile.Name())
}
func printFileContent(filename string) {
data, _ := os.ReadFile(filename)
fmt.Println("Содержимое файла:")
fmt.Println(string(data))
fmt.Println("Длина:", len(data), "байт")
fmt.Println(strings.Repeat("-", 50))
}
Разбор функциональности:
-
Write(p []byte):
- Записывает данные с текущей позиции
- Автоматически увеличивает текущую позицию
-
Seek(offset, whence):
whence
может быть:io.SeekStart
- от начала файлаio.SeekCurrent
- от текущей позицииio.SeekEnd
- от конца файла
- Возвращает новую позицию
-
WriteAt(p []byte, off int64):
- Записывает данные по абсолютному смещению
off
- Не изменяет текущую позицию записи
- Полезен для точечных модификаций
- Записывает данные по абсолютному смещению
Пример вывода:
1. Запись через Write()
Содержимое файла:
=== Начало файла ===
ПЕРВАЯ ЗАПИСЬ....................
=== Конец файла ===
Длина: 132 байт
--------------------------------------------------
2. Seek(10, io.SeekCurrent)
Новая позиция: 42
3. Запись через Write()
Содержимое файла:
=== Начало файла ===
ПЕРВАЯ ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
4. WriteAt() по смещению 5
Содержимое файла:
=== Начало файла ===
ПЕРТЬЯ ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
5. Текущая позиция после WriteAt: 42
6. Seek(0, io.SeekStart) + Write()
Содержимое файла:
=== Начало файла ===
НАЧАЛО ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
Практические сценарии использования:
-
Редактирование файлов:
// Исправление заголовка в существующем файле ow := io.NewOffsetWriter(file, 0) ow.WriteAt(correctHeader, 0)
-
Работа с бинарными форматами:
// Запись данных в определенные секции файла ow.Seek(headerSize, io.SeekStart) ow.Write(dataChunk)
-
Многопоточная запись:
// Каждая горутина пишет в свой раздел go func() { sectionWriter := io.NewOffsetWriter(file, sectionOffset) sectionWriter.Write(sectionData) }()
func NewOffsetWriter
func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter
NewOffsetWriter возвращает OffsetWriter, который записывает в w, начиная с смещения off.
func (*OffsetWriter) Seek
func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error)
func (*OffsetWriter) Write
func (o *OffsetWriter) Write(p []byte) (n int, err error)
func (*OffsetWriter) WriteAt
func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error)
type PipeReader
type PipeReader struct {
// содержит отфильтрованные или неэкспортируемые поля
}
PipeReader
— это читающая часть канала (pipe).
Объяснение PipeReader и PipeWriter
PipeReader
представляет читающую часть именованного канала (pipe).
PipeWriter
представляет записывающую часть именованного канала (pipe).
package main
import (
"fmt"
"io"
"time"
)
func main() {
// Создаем pipe
pipeReader, pipeWriter := io.Pipe()
// Горутина для чтения из канала
go func() {
buf := make([]byte, 256)
// 1. Обычное чтение
n, err := pipeReader.Read(buf)
if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
return
}
fmt.Printf("Прочитано %d байт: %s\n", n, buf[:n])
// 2. Попытка чтения после закрытия записи
n, err = pipeReader.Read(buf)
if err == io.EOF {
fmt.Println("Канал закрыт (EOF)")
} else if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
}
// 3. Чтение после CloseWithError
_, err = pipeReader.Read(buf)
if err != nil {
fmt.Printf("Ошибка после CloseWithError: %v\n", err)
}
}()
// Горутина для записи в канал
go func() {
// Записываем данные
_, err := pipeWriter.Write([]byte("Тестовые данные"))
if err != nil {
fmt.Printf("Ошибка записи: %v\n", err)
}
// Закрываем запись (обычное закрытие)
pipeWriter.Close()
// Даем время на обработку
time.Sleep(100 * time.Millisecond)
// Пытаемся записать после закрытия чтения
_, err = pipeWriter.Write([]byte("Новые данные"))
if err == io.ErrClosedPipe {
fmt.Println("Попытка записи в закрытый канал (ErrClosedPipe)")
}
}()
// Даем время на выполнение
time.Sleep(200 * time.Millisecond)
// Закрываем читатель с ошибкой
err := pipeReader.CloseWithError(fmt.Errorf("кастомная ошибка"))
if err != nil {
fmt.Printf("Ошибка при CloseWithError: %v\n", err)
}
// Пытаемся записать после CloseWithError
_, err = pipeWriter.Write([]byte("Последние данные"))
if err != nil {
fmt.Printf("Ошибка записи после CloseWithError: %v\n", err)
}
time.Sleep(100 * time.Millisecond)
}
Разбор методов:
-
Read():
- Блокируется, пока не появятся данные или не закроется записывающая часть
- При закрытии записывающей части возвращает
io.EOF
- При
CloseWithError
возвращает указанную ошибку
-
Close():
- Закрывает читающую часть
- Последующие записи будут возвращать
io.ErrClosedPipe
-
CloseWithError():
- Закрывает читающую часть с указанной ошибкой
- Последующие операции чтения вернут эту ошибку
- Не перезаписывает предыдущие ошибки
Пример вывода:
Прочитано 28 байт: Тестовые данные
Канал закрыт (EOF)
Попытка записи в закрытый канал (ErrClosedPipe)
Ошибка после CloseWithError: кастомная ошибка
Ошибка записи после CloseWithError: io: read/write on closed pipe
Практическое применение:
- Связь между горутинами
- Преобразование данных “на лету”
- Тестирование кода, работающего с интерфейсами
io.Reader
/io.Writer
- Реализация прокси-серверов и middleware
func (*PipeReader) Close
func (r *PipeReader) Close() error
Close закрывает читатель; последующие записи в записывающую половину канала будут возвращать ошибку ErrClosedPipe.
func (*PipeReader) CloseWithError
func (r *PipeReader) CloseWithError(err error) error
CloseWithError закрывает читатель; последующие записи в записывающую половину канала будут возвращать ошибку err.
CloseWithError никогда не перезаписывает предыдущую ошибку, если она существует, и всегда возвращает nil.
func (*PipeReader) Read
func (r *PipeReader) Read(data []byte) (n int, err error)
Read реализует стандартный интерфейс Read: он считывает данные из канала, блокируя до тех пор, пока не появится записывающее устройство или не будет закрыта записывающая часть. Если записывающая часть закрыта с ошибкой, эта ошибка возвращается как err; в противном случае err равен EOF.
type PipeWriter
type PipeWriter struct {
// содержит отфильтрованные или неэкспортируемые поля
}
PipeWriter — это записывающая часть канала (pipe).
func (*PipeWriter) Close
func (w *PipeWriter) Close() error
Close закрывает записывающее устройство; последующие чтения из читающей половины канала не будут возвращать байты и EOF.
func (*PipeWriter) CloseWithError
func (w *PipeWriter) CloseWithError(err error) error
CloseWithError закрывает запись; последующие чтения из части трубы, отвечающей за чтение, не будут возвращать байты и будут возвращать ошибку err или EOF, если err равна nil.
CloseWithError никогда не перезаписывает предыдущую ошибку, если она существует, и всегда возвращает nil.
func (*PipeWriter) Write
func (w *PipeWriter) Write(data []byte) (n int, err error)
Write реализует стандартный интерфейс Write: он записывает данные в канал, блокируя его до тех пор, пока один или несколько читателей не потребят все данные или читающая сторона не будет закрыта. Если читающая сторона закрыта с ошибкой, эта ошибка возвращается как err; в противном случае err равна ErrClosedPipe.
type ReadCloser
type ReadCloser интерфейс {
Reader
Closer
}
ReadCloser — это интерфейс, который группирует основные методы Read и Close.
Объяснение ReadCloser
ReadCloser
- это просто комбинация двух возможностей:
- Чтение данных (как у обычного
Reader
) - Закрытие ресурса (как у
Closer
)
Это интерфейс для объектов, которые:
- Могут отдавать данные (например, файл, сетевое соединение)
- И требуют закрытия после использования (чтобы освободить ресурсы)
Примеры использования:
- Открыли файл → читаем из него → закрываем
- Установили сетевое соединение → получаем данные → разрываем соединение
NopCloser
- это “адаптер”:
- Берет обычный
Reader
(который не умеет закрываться) - Возвращает
ReadCloser
, где методClose()
ничего не делает (“no-op” - операция-пустышка)
Зачем это нужно?
Когда функция требует ReadCloser
, а у вас есть только Reader
(который не нужно закрывать)
Пример:
// Есть строка (она реализует Reader)
data := strings.NewReader("Привет, мир!")
// Но нам нужно передать ReadCloser
rc := io.NopCloser(data)
// Теперь можно использовать везде, где требуется ReadCloser
// При вызове rc.Close() ничего не произойдет
Где применяется:
- Когда работаем с API, требующим
ReadCloser
- Когда нужно подставить “фейковый” closer для тестирования
- При работе с данными в памяти, которые не требуют очистки
func NopCloser
func NopCloser(r Reader) ReadCloser
NopCloser возвращает ReadCloser с методом Close, не выполняющим никаких действий, который оборачивает предоставленный Reader r. Если r реализует WriterTo, возвращаемый ReadCloser будет реализовывать WriterTo, перенаправляя вызовы r.
type ReadSeekCloser
type ReadSeekCloser интерфейс {
Reader
Seeker
Closer
}
ReadSeekCloser — интерфейс, который группирует основные методы Read, Seek и Close.
Объяснение ReadSeekCloser, ReadSeeker, ReadWriteCloser…
1. ReadSeekCloser
Назначение: Объединяет чтение, произвольный доступ и закрытие ресурса.
Пример с файлом:
func processFile(rsc io.ReadSeekCloser) error {
data := make([]byte, 100)
// Чтение
if _, err := rsc.Read(data); err != nil {
return err
}
// Перемещение в начало
if _, err := rsc.Seek(0, io.SeekStart); err != nil {
return err
}
// Закрытие
defer rsc.Close()
return nil
}
// Использование
file, _ := os.Open("data.txt")
processFile(file) // *os.File реализует ReadSeekCloser
Зачем: Нужен для работы с ресурсами (файлы, сетевые потоки), где требуется:
- Чтение данных
- Перемещение по содержимому
- Обязательное закрытие
2. ReadSeeker
Назначение: Чтение + перемещение по данным без закрытия.
Пример с буфером:
func analyze(data io.ReadSeeker) {
// Первое чтение
buf := make([]byte, 10)
data.Read(buf)
// Возврат в начало
data.Seek(0, io.SeekStart)
}
// Использование
buffer := strings.NewReader("abcdefghij")
analyze(buffer) // strings.Reader реализует ReadSeeker
Зачем: Для данных, где нужно:
- Многократное чтение
- Навигация (например, парсинг заголовков)
3. ReadWriteCloser
Назначение: Чтение + запись + закрытие.
Пример с сетевым соединением:
func handleConnection(conn io.ReadWriteCloser) {
defer conn.Close()
// Чтение
buf := make([]byte, 1024)
conn.Read(buf)
// Ответ
conn.Write([]byte("OK"))
}
// Использование (псевдокод)
// conn, _ := net.Dial("tcp", "example.com:80")
// handleConnection(conn) // net.Conn реализует ReadWriteCloser
Зачем: Для двусторонних ресурсов:
- Файлы с записью
- Сетевые соединения
- Драйверы устройств
4. ReadWriteSeeker
Назначение: Чтение + запись + навигация.
Пример с файлом логов:
func updateLog(rws io.ReadWriteSeeker, msg string) {
// Перемещение в конец
rws.Seek(0, io.SeekEnd)
// Запись
rws.Write([]byte(msg))
}
// Использование
file, _ := os.OpenFile("log.txt", os.O_RDWR, 0644)
updateLog(file, "New entry\n")
Зачем: Для редактируемых ресурсов:
- Файлы баз данных
- Логи
- Память с произвольным доступом
5. ReadWriter
Назначение: Только чтение + запись.
Пример с буфером в памяти:
func process(rw io.ReadWriter) {
rw.Write([]byte("ping"))
response, _ := io.ReadAll(rw)
fmt.Println(string(response))
}
// Использование
var buf bytes.Buffer
buf.WriteString("pong")
process(&buf) // bytes.Buffer реализует ReadWriter
Зачем: Для простых двусторонних потоков:
- Буферы
- Шифрование/сжатие “на лету”
- Тестирование
Ключевые отличия:
Тип | Read | Write | Seek | Close | Примеры использования |
---|---|---|---|---|---|
ReadSeekCloser |
✓ | ✗ | ✓ | ✓ | Файлы, сетевые потоки |
ReadSeeker |
✓ | ✗ | ✓ | ✗ | Парсинг данных |
ReadWriteCloser |
✓ | ✓ | ✗ | ✓ | Сокеты, открытые файлы на запись |
ReadWriteSeeker |
✓ | ✓ | ✓ | ✗ | Файлы БД, лог-файлы |
ReadWriter |
✓ | ✓ | ✗ | ✗ | Буферы, преобразователи данных |
type ReadSeeker
type ReadSeeker интерфейс {
Reader
Seeker
}
ReadSeeker — интерфейс, который группирует основные методы Read и Seek.
type ReadWriteCloser
type ReadWriteCloser интерфейс {
Reader
Writer
Closer
}
ReadWriteCloser — это интерфейс, который группирует основные методы Read, Write и Close.
type ReadWriteSeeker
type ReadWriteSeeker интерфейс {
Reader
Writer
Seeker
}
ReadWriteSeeker — интерфейс, который группирует основные методы Read, Write и Seek.
type ReadWriter
type ReadWriter интерфейс {
Reader
Writer
}
ReadWriter — интерфейс, который группирует основные методы Read и Write.
type Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader — это интерфейс, который оборачивает базовый метод Read.
Read считывает до len(p) байт в p. Он возвращает количество прочитанных байтов (0 <= n <= len(p)) и любую возникшую ошибку. Даже если Read возвращает n < len(p), он может использовать все p в качестве временного пространства во время вызова. Если некоторые данные доступны, но не len(p) байтов, Read по умолчанию возвращает то, что доступно, вместо того, чтобы ждать больше.
Когда Read встречает ошибку или условие конца файла после успешного чтения n > 0 байтов, он возвращает количество прочитанных байтов. Он может вернуть ошибку (не nil) из того же вызова или вернуть ошибку (и n == 0) из последующего вызова. Примером этого общего случая является то, что Reader, возвращающий ненулевое количество байтов в конце входного потока, может возвращать либо err == EOF, либо err == nil. Следующий Read должен возвращать 0, EOF.
Вызывающие функции должны всегда обрабатывать возвращенные n > 0 байтов, прежде чем рассматривать ошибку err. Это позволяет правильно обрабатывать ошибки ввода-вывода, возникающие после чтения некоторых байтов, а также оба допустимых поведения EOF.
Если len(p) == 0, Read всегда должен возвращать n == 0. Он может возвращать ошибку, отличную от nil, если известно о каком-либо условии ошибки, таком как EOF.
Реализации Read не рекомендуется возвращать нулевое количество байтов с ошибкой nil, за исключением случаев, когда len(p) == 0. Вызывающие должны рассматривать возвращение 0 и nil как указание на то, что ничего не произошло; в частности, это не указывает на EOF.
Реализации не должны сохранять p.
func LimitReader
func LimitReader(r Reader, n int64) Reader
LimitReader возвращает Reader, который читает из r, но останавливается с EOF после n байтов. Базовой реализацией является *LimitedReader.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
lr := io.LimitReader(r, 4)
if _, err := io.Copy(os.Stdout, lr); err != nil {
log.Fatal(err)
}
}
Output:
some
func MultiReader
func MultiReader(readers ...Reader) Reader
MultiReader возвращает Reader, который является логическим соединением предоставленных входных считывателей. Они считываются последовательно. Как только все входы вернут EOF, Read вернет EOF. Если какой-либо из считывателей вернет ошибку, отличную от nil и EOF, Read вернет эту ошибку.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r1 := strings.NewReader("first reader ")
r2 := strings.NewReader("second reader ")
r3 := strings.NewReader("third reader\n")
r := io.MultiReader(r1, r2, r3)
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
first reader second reader third reader
func TeeReader
func TeeReader(r Reader, w Writer) Reader
TeeReader возвращает Reader, который записывает в w то, что он читает из r. Все чтения из r, выполняемые через него, сопоставляются с соответствующими записями в w. Внутренней буферизации нет — запись должна быть завершена до завершения чтения. Любая ошибка, возникшая во время записи, сообщается как ошибка чтения.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
var r io.Reader = strings.NewReader("some io.Reader stream to be read\n")
r = io.TeeReader(r, os.Stdout)
// Everything read from r will be copied to stdout.
if _, err := io.ReadAll(r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
type ReaderAt
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
ReaderAt — это интерфейс, который оборачивает базовый метод ReadAt.
ReadAt считывает len(p) байт в p, начиная с смещения off в базовом источнике ввода. Он возвращает количество прочитанных байтов (0 <= n <= len(p)) и любую возникшую ошибку.
Когда ReadAt возвращает n < len(p), он возвращает не нулевую ошибку, объясняющую, почему не было возвращено больше байтов. В этом отношении ReadAt более строг, чем Read.
Даже если ReadAt возвращает n < len(p), он может использовать все p в качестве временного пространства во время вызова. Если некоторые данные доступны, но не len(p) байтов, ReadAt блокируется до тех пор, пока не будут доступны все данные или не произойдет ошибка. В этом отношении ReadAt отличается от Read.
Если n = len(p) байтов, возвращаемых ReadAt, находятся в конце источника ввода, ReadAt может вернуть либо err == EOF, либо err == nil.
Если ReadAt читает из источника ввода с смещением поиска, ReadAt не должен влиять на базовое смещение поиска и не должен подвергаться его влиянию.
Клиенты ReadAt могут выполнять параллельные вызовы ReadAt на одном и том же источнике ввода.
Реализации не должны сохранять p.
Объяснение ReaderAt
Интерфейс ReaderAt
в Go
Интерфейс ReaderAt
определён в пакете io
и позволяет читать данные из произвольного смещения (offset
) без изменения состояния “читателя”.
Сигнатура метода
ReadAt(p []byte, off int64) (n int, err error)
p []byte
— буфер, куда записываются прочитанные данные.off int64
— смещение (в байтах) от начала источника данных.- Возвращает:
n
— количество прочитанных байтов.err
— ошибку (например,io.EOF
при достижении конца данных).
Пример использования ReaderAt
1. Чтение файла с произвольного смещения
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
panic(err)
}
defer file.Close()
// Читаем 10 байт, начиная с 5-го байта
buf := make([]byte, 10)
n, err := file.ReadAt(buf, 5)
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("Прочитано %d байт: %q\n", n, buf[:n])
}
Вывод (если example.txt
содержит "Hello, world!"
):
2. Реализация собственного ReaderAt
Допустим, у нас есть структура, хранящая данные в памяти:
type MemoryReader struct {
data []byte
}
func (r *MemoryReader) ReadAt(p []byte, off int64) (n int, err error) {
if off >= int64(len(r.data)) {
return 0, io.EOF
}
n = copy(p, r.data[off:])
if n < len(p) {
err = io.EOF
}
return
}
func main() {
reader := &MemoryReader{data: []byte("RandomAccessData")}
buf := make([]byte, 5)
// Читаем 5 байт с 7-й позиции
n, err := reader.ReadAt(buf, 7)
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("Прочитано: %q\n", buf[:n]) // "Access"
}
Назначение ReaderAt
- Произвольный доступ (random access) — чтение с любого места без последовательного перемещения.
- Потокобезопасность — метод
ReadAt
можно вызывать из разных горутин (если реализация поддерживает). - Используется в:
- Файловых системах (
os.File
реализуетReaderAt
). - Бинарных форматах (например, чтение заголовков архивов).
- Базах данных (чтение данных по смещению).
- Файловых системах (
Отличие от Reader
Reader (io.Reader ) |
ReaderAt (io.ReaderAt ) |
---|---|
Читает последовательно (Read меняет состояние). |
Читает с любого места (ReadAt не меняет состояние). |
Используется в потоковых данных (сети, pipes). | Используется для произвольного доступа (файлы, память). |
Пример: http.Response.Body . |
Пример: os.File . |
Вывод
ReaderAt
полезен, когда нужно читать данные с произвольных позиций, например, при работе с файлами или бинарными структурами. В отличие от Reader
, он не зависит от текущей позиции и поддерживает конкурентный доступ.
type ReaderFrom
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
ReaderFrom — это интерфейс, который оборачивает метод ReadFrom.
ReadFrom читает данные из r до EOF или ошибки. Возвращаемое значение n — это количество прочитанных байтов. Любая ошибка, кроме EOF, возникшая во время чтения, также возвращается.
Функция Copy использует ReaderFrom, если он доступен.
type RuneReader
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}
RuneReader — это интерфейс, который оборачивает метод ReadRune.
ReadRune считывает один закодированный символ Unicode и возвращает руну и ее размер в байтах. Если символ недоступен, будет установлено err.
type RuneScanner
type RuneScanner interface {
RuneReader
UnreadRune() error
}
RuneScanner — это интерфейс, который добавляет метод UnreadRune к базовому методу ReadRune.
UnreadRune заставляет следующий вызов ReadRune возвращать последнюю прочитанную руну. Если последняя операция не была успешным вызовом ReadRune, UnreadRune может вернуть ошибку, не прочитать последнюю прочитанную руну (или руну, предшествующую последней непрочитанной руне), или (в реализациях, поддерживающих интерфейс Seeker) перейти к началу руны перед текущим смещением.
type SectionReader
type SectionReader struct {
// содержит отфильтрованные или неэкспортированные поля
}
SectionReader реализует Read, Seek и ReadAt на секции базового ReaderAt.
Объяснение SectionReader
Назначение SectionReader
SectionReader
в Go (пакет io
) позволяет читать только определённую часть (секцию
) данных из базового источника, реализующего ReaderAt
. Это полезно, когда нужно:
- Ограничить чтение определённым диапазоном байтов
- Работать с частью файла или данных как с самостоятельным потоком
- Читать данные с произвольных позиций (
ReadAt
) и перемещаться по ним (Seek
)
Пример использования всех методов
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// Исходные данные
data := "Hello, World! This is a SectionReader example."
baseReader := strings.NewReader(data)
// Создаём SectionReader (база: data, смещение: 7, длина: 12 байтов)
section := io.NewSectionReader(baseReader, 7, 12)
// 1. Чтение всего раздела (Read)
buf := make([]byte, 12)
n, _ := section.Read(buf)
fmt.Printf("Read: %q (%d bytes)\n", buf[:n], n) // "World! This"
// 2. Чтение с позиции (ReadAt)
n, _ = section.ReadAt(buf, 2)
fmt.Printf("ReadAt(2): %q\n", buf[:n]) // "rld! This "
// 3. Перемещение (Seek)
section.Seek(5, io.SeekStart) // Перемещаемся на 5 байт от начала
n, _ = section.Read(buf)
fmt.Printf("After Seek(5): %q\n", buf[:n]) // "! This"
// 4. Получение параметров секции (Outer)
r, off, n := section.Outer()
fmt.Printf("Base: %T, Offset: %d, Size: %d\n", r, off, n)
// 5. Размер секции (Size)
fmt.Println("Section size:", section.Size()) // 12
}
Вывод:
Read: "World! This" (12 bytes)
ReadAt(2): "rld! This "
After Seek(5): "! This"
Base: *strings.Reader, Offset: 7, Size: 12
Section size: 12
Разбор методов
Метод | Описание |
---|---|
NewSectionReader() |
Создаёт читатель для диапазона [off, off+n) из базового ReaderAt |
Read() |
Читает данные последовательно (меняет текущую позицию) |
ReadAt() |
Читает данные с указанного смещения (не меняет текущую позицию) |
Seek() |
Перемещает текущую позицию (io.SeekStart/SeekCurrent/SeekEnd ) |
Size() |
Возвращает максимальный размер секции в байтах |
Outer() |
Возвращает исходный ReaderAt , смещение и размер, переданные при создании |
Типичные сценарии использования
-
Чтение заголовков файлов
// Читаем первые 512 байт (например, заголовок ZIP) headerSection := io.NewSectionReader(file, 0, 512)
-
Обработка частей больших файлов
// Читаем блок с 1024 по 2048 байт chunk := io.NewSectionReader(file, 1024, 1024)
-
Виртуализация подмножества данных
// Работаем с частью данных как с самостоятельным io.Reader processData(section)
SectionReader
особенно полезен при работе с бинарными форматами (архивы, медиафайлы), где нужно читать данные по чанкам или с определённых смещений.
func NewSectionReader
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader
NewSectionReader возвращает SectionReader, который читает из r, начиная с смещения off, и останавливается с EOF после n байтов.
func (*SectionReader) Outer
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64)
Outer возвращает базовый ReaderAt и смещения для секции.
Возвращаемые значения совпадают с теми, которые были переданы в NewSectionReader при создании SectionReader.
func (*SectionReader) Read
func (s *SectionReader) Read(p []byte) (n int, err error)
Пример
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
buf := make([]byte, 9)
if _, err := s.Read(buf); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
}
Output:
io.Reader
func (*SectionReader) ReadAt
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error)
Пример
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
buf := make([]byte, 6)
if _, err := s.ReadAt(buf, 10); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
}
Output:
stream
func (*SectionReader) Seek
func (s *SectionReader) Seek(offset int64, whence int) (int64, error)
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
if _, err := s.Seek(10, io.SeekStart); err != nil {
log.Fatal(err)
}
if _, err := io.Copy(os.Stdout, s); err != nil {
log.Fatal(err)
}
}
Output:
stream
func (*SectionReader) Size
func (s *SectionReader) Size() int64
Size возвращает размер секции в байтах.
Пример
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
fmt.Println(s.Size())
}
Output:
17
type Seeker
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
Seeker — это интерфейс, который оборачивает базовый метод Seek.
Seek устанавливает смещение для следующего Read или Write в offset, интерпретируемое в соответствии с whence: SeekStart означает относительно начала файла, SeekCurrent означает относительно текущего смещения, а SeekEnd означает относительно конца (например, offset = -2 указывает предпоследний байт файла). Seek возвращает новое смещение относительно начала файла или ошибку, если таковая имеется.
Переход к смещению перед началом файла является ошибкой. Переход к любому положительному смещению может быть разрешен, но если новое смещение превышает размер базового объекта, поведение последующих операций ввода-вывода зависит от реализации.
Объяснение Seeker
Назначение интерфейса Seeker
в пакете io
Интерфейс Seeker
определяет метод Seek()
, который позволяет произвольно перемещаться по потоку данных (файлу, буферу в памяти, сетевому соединению и т.д.). Это ключевая возможность для работы с данными не только последовательно, но и в произвольном порядке.
Для чего нужен Seeker
?
-
Произвольный доступ к данным
Возможность “перепрыгивать” к любому месту в потоке без чтения предыдущих данных. -
Чтение/запись в разные части файла
Например, обновление заголовка файла после записи основного содержимого. -
Реализация сложных протоколов
Парсинг структур, где нужно “заглядывать” вперед и возвращаться назад. -
Оптимизация работы с большими файлами
Чтение только нужных фрагментов без загрузки всего файла в память.
Детали метода Seek()
Seek(offset int64, whence int) (int64, error)
Параметр | Значения (whence ) |
Описание |
---|---|---|
offset |
Любое число | Смещение в байтах (может быть отрицательным для SeekCurrent/SeekEnd ) |
whence |
io.SeekStart (0) |
Отсчёт от начала данных |
io.SeekCurrent (1) |
Отсчёт от текущей позиции | |
io.SeekEnd (2) |
Отсчёт от конца данных |
Возвращает:
- Новую позицию (абсолютный offset от начала)
- Ошибку (например, попытка выйти за границы данных)
Примеры использования
1. Перемещение в файле
file, _ := os.Open("data.txt")
defer file.Close()
// Перемещаемся на 100-й байт от начала
pos, _ := file.Seek(100, io.SeekStart)
// Читаем 50 байт с этой позиции
buf := make([]byte, 50)
file.Read(buf)
2. Чтение с конца
// Перемещаемся на 10 байт назад от конца
pos, _ := file.Seek(-10, io.SeekEnd)
3. Относительное перемещение
// Текущая позиция: 200
// Перемещаемся на +50 байт вперёд
pos, _ := file.Seek(50, io.SeekCurrent) // Новый pos = 250
Где реализован Seeker
?
Стандартные типы, поддерживающие интерфейс:
*os.File
(файлы)*bytes.Reader
(буфер в памяти)*strings.Reader
(строка как поток)*SectionReader
(часть другогоReaderAt
)
Ограничения
-
Не все источники поддерживают
Например, сетевые соединения (net.Conn
) не реализуютSeeker
. -
Поведение зависит от типа
При работе с файлами на дискеSeek
эффективен, но для сжатых данных (например, ZIP) может требовать декомпрессии.
Комбинация с другими интерфейсами
Часто используется вместе с:
Reader
→io.ReadSeeker
Writer
→io.WriteSeeker
ReaderAt
/WriterAt
для произвольного доступа без изменения позиции
Пример объединённого интерфейса:
type ReadSeeker interface {
Reader
Seeker
}
Итог
Seeker
добавляет потокам данных критически важную возможность — произвольный доступ. Это фундамент для:
- Работы с бинарными форматами (архивы, медиафайлы)
- Оптимизированного чтения больших файлов
- Реализации сложных алгоритмов парсинга
- Многопроходной обработки данных без переоткрытия источника
type StringWriter
type StringWriter interface {
WriteString(s string) (n int, err error)
}
StringWriter — это интерфейс, который оборачивает метод WriteString.
type WriteCloser
type WriteCloser interface {
Writer
Closer
}
WriteCloser — это интерфейс, который группирует базовые методы Write и Close.
type WriteSeeker
type WriteSeeker interface {
Writer
Seeker
}
WriteSeeker — это интерфейс, который группирует базовые методы Write и Seek.
type Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
Writer — это интерфейс, который оборачивает базовый метод Write.
Write записывает len(p) байт из p в базовый поток данных. Он возвращает количество байт, записанных из p (0 <= n <= len(p)), и любую ошибку, которая привела к преждевременному прекращению записи. Write должен возвращать ошибку, отличную от nil, если он возвращает n < len(p). Write не должен изменять данные слайса, даже временно.
Реализации не должны сохранять p.
var Discard Writer = discard{}
Discard — это Writer, на котором все вызовы Write выполняются успешно, не выполняя никаких действий.
Объяснение Writer
Интерфейс Writer
в Go — это простой контракт для записи данных куда-либо. Он требует реализации всего одного метода:
Write(p []byte) (n int, err error)
.
Как это работает?
-
Вы передаёте данные
Метод принимает байтовый слайс (p []byte
), который нужно записать (например: текст, файл, сетевой пакет). -
Он пытается записать
Записывает часть или все данные изp
в целевое место (файл, память, сеть и т.д.). -
Возвращает результат
n
— сколько байт удалось записать (может быть меньше, чемlen(p)
при ошибке).err
— ошибка (например, диск заполнен, соединение разорвано).
Аналогия из жизни
Представьте, что Writer
— это лестница в доме:
- Люди постепенно заходят на лестницу и поднимаются вверх (
p []byte
). - На последнем этаже большой зал, люди заполняют его.
- Если зал заполнился полностью людьми, то часть останется на лестнице и в зал не войдут (
n < len(p)
иerr != nil
).
Где используется?
Примеры реализаций:
-
Запись в файл
file, _ := os.Create("log.txt") file.Write([]byte("Hello!")) // Реализует Writer
-
Отправка данных по сети
conn, _ := net.Dial("tcp", "example.com:80") conn.Write([]byte("GET / HTTP/1.1\r\n\r\n"))
-
Буфер в памяти
var buf bytes.Buffer buf.Write([]byte("Сохраняем в RAM"))
-
Игнорирование данных (
Discard
)io.Discard.Write([]byte("Эти данные никуда не пойдут"))
Правила для Write
-
Не изменяет
p
Даже временно. Ваши исходные данные останутся целыми. -
Не сохраняет
p
После завершения метода реализация не должна хранить ссылку на переданные данные. -
Ошибка = неполная запись
Если вернулосьn < len(p)
, метод обязан вернуть ошибку.
Особый случай: Discard
Это “пустышка”, которая реализует Writer
, но просто выбрасывает все записываемые данные:
io.Discard.Write([]byte("Это исчезнет")) // Никуда не запишется
Зачем нужно? Например:
- Когда нужно прочитать данные, но не сохранять их.
- Для тестирования, чтобы имитировать запись без реальных операций.
Пример с кастомным Writer
Создадим простой Writer
, который пишет данные в консоль:
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(p []byte) (int, error) {
n, err := fmt.Print(string(p))
return n, err
}
func main() {
var writer Writer = ConsoleWriter{}
writer.Write([]byte("Привет, Writer!"))
}
Вывод:
Привет, Writer!
func MultiWriter
func MultiWriter(writers ...Writer) Writer
MultiWriter создает писатель, который дублирует свои записи во всех предоставленных писателях, аналогично команде Unix tee(1).
Каждая запись записывается в каждый из перечисленных writer, по одному за раз. Если перечисленный writer возвращает ошибку, вся операция записи останавливается и возвращает ошибку; она не продолжается по списку.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
var buf1, buf2 strings.Builder
w := io.MultiWriter(&buf1, &buf2)
if _, err := io.Copy(w, r); err != nil {
log.Fatal(err)
}
fmt.Print(buf1.String())
fmt.Print(buf2.String())
}
Output:
some io.Reader stream to be read
some io.Reader stream to be read
type WriterAt
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
WriterAt — это интерфейс, который оборачивает базовый метод WriteAt.
WriteAt записывает len(p) байт из p в базовый поток данных со смещением off. Он возвращает количество байт, записанных из p (0 <= n <= len(p)), и любую ошибку, которая привела к преждевременному прекращению записи. WriteAt должен возвращать ошибку, отличную от nil, если он возвращает n < len(p).
Если WriteAt записывает в место назначения со смещением поиска, WriteAt не должен влиять на базовое смещение поиска и не должен подвергаться его влиянию.
Клиенты WriteAt могут выполнять параллельные вызовы WriteAt на одном и том же месте назначения, если диапазоны не пересекаются.
Реализации не должны сохранять p.
type WriterTo
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
WriterTo — это интерфейс, который оборачивает метод WriteTo.
WriteTo записывает данные в w до тех пор, пока не закончатся данные для записи или не произойдет ошибка. Возвращаемое значение n — это количество записанных байтов. Любая ошибка, возникшая во время записи, также возвращается.
Функция Copy использует WriterTo, если он доступен.
Объяснение WriterTo
Интерфейс WriterTo
— это продвинутая версия записи данных, которая позволяет объекту самому решать, как эффективно отправить свои данные в любой Writer
(файл, сеть, буфер и т.д.).
Чем отличается от обычного Writer
?
Особенность | Writer (простой интерфейс) |
WriterTo (продвинутый интерфейс) |
---|---|---|
Кто управляет? | Получатель (Writer ) решает, как записать данные |
Источник данных сам решает, как отправить данные |
Эффективность | Может требовать промежуточных копирований | Позволяет оптимизировать запись (например, отправлять данные кусками) |
Использование | Базовый уровень | Оптимизированные сценарии (например, io.Copy ) |
Как работает метод WriteTo
?
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
-
Вызывается у объекта-источника
Объект (например, буфер или файл) получает целевойWriter
(w
), куда нужно записать данные. -
Сам решает, как писать
Источник может:- Отправить данные одним куском
- Разбить на части
- Использовать специальные оптимизации (например, DMA для дисков)
-
Возвращает результат
n
— сколько байт было записаноerr
— ошибка (если что-то пошло не так)
Примеры использования
1. Стандартные типы с WriterTo
// bytes.Buffer реализует WriterTo
buf := bytes.NewBufferString("Hello!")
buf.WriteTo(os.Stdout) // Выведет "Hello!" в консоль
// strings.Reader тоже реализует WriterTo
reader := strings.NewReader("Text")
reader.WriteTo(file) // Запишет "Text" в файл
2. Кастомная реализация
Создадим объект, который знает, как эффективно записать свои данные:
type CustomData struct {
chunks [][]byte
}
func (d *CustomData) WriteTo(w io.Writer) (int64, error) {
var total int64
for _, chunk := range d.chunks {
n, err := w.Write(chunk)
total += int64(n)
if err != nil {
return total, err
}
}
return total, nil
}
// Использование:
data := CustomData{chunks: [][]byte{[]byte("Hello "), []byte("world!")}}
data.WriteTo(os.Stdout) // Выведет "Hello world!"
Зачем это нужно?
-
Оптимизация производительности
Объект может выбрать самый эффективный способ записи (например, избежать лишних копирований). -
Гибкость
Разные типы данных могут по-разному реализовывать запись:- Файл может отправлять данные кусками
- Буфер в памяти — одним вызовом
- Сетевое соединение — с контролем скорости
-
Интеграция с
io.Copy
Функцияio.Copy(dst, src)
автоматически используетWriteTo
, если он есть у источника:// Если src реализует WriterTo, Copy вызовет src.WriteTo(dst) io.Copy(file, buffer) // Будет эффективнее, чем обычное копирование
Особый случай: Discard
Как и с Writer
, существует “пустышка”:
io.Discard.WriteTo(...) // Ничего не делает
Используется для тестирования или игнорирования данных.