1 - Описание функций пакета io
Спецификация функций пакета io с примерами на языке Go
func Copy
func Copy(dst Writer, src Reader) (written int64, err error)
Copy копирует данные из src в dst до тех пор, пока не будет достигнут EOF в src или не произойдет ошибка. Функция возвращает количество скопированных байтов и первую ошибку, возникшую при копировании, если таковая имеется.
Успешная функция Copy возвращает err == nil, а не err == EOF. Поскольку функция Copy определена для чтения из src до EOF, она не рассматривает EOF из Read как ошибку, о которой следует сообщать.
Если src реализует WriterTo, копирование реализуется вызовом src.WriteTo(dst). В противном случае, если dst реализует ReaderFrom, копирование реализуется вызовом dst.ReadFrom(src).
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
func CopyBuffer
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
CopyBuffer идентична Copy, за исключением того, что она проходит через предоставленный буфер (если он требуется), а не выделяет временный. Если buf равна nil, выделяется один; в противном случае, если она имеет нулевую длину, CopyBuffer вызывает панику.
Если src реализует WriterTo или dst реализует ReaderFrom, buf не будет использоваться для выполнения копирования.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r1 := strings.NewReader("first reader\n")
r2 := strings.NewReader("second reader\n")
buf := make([]byte, 8)
// buf is used here...
if _, err := io.CopyBuffer(os.Stdout, r1, buf); err != nil {
log.Fatal(err)
}
// ... reused here also. No need to allocate an extra buffer.
if _, err := io.CopyBuffer(os.Stdout, r2, buf); err != nil {
log.Fatal(err)
}
}
Output:
first reader
second reader
func CopyN
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
CopyN копирует n байт (или до возникновения ошибки) из src в dst. Он возвращает количество скопированных байтов и самую раннюю ошибку, возникшую во время копирования. При возвращении written == n тогда и только тогда, когда err == nil.
Если dst реализует ReaderFrom, копирование реализуется с его помощью.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read")
if _, err := io.CopyN(os.Stdout, r, 4); err != nil {
log.Fatal(err)
}
}
func Pipe
func Pipe() (*PipeReader, *PipeWriter)
Pipe создает синхронный конвейер в памяти. Его можно использовать для соединения кода, ожидающего io.Reader, с кодом, ожидающим io.Writer.
Чтение и запись в канале сопоставляются один к одному, за исключением случаев, когда для потребления одной записи требуется несколько чтений. То есть каждая запись в PipeWriter блокируется до тех пор, пока не будет удовлетворено одно или несколько чтений из PipeReader, которые полностью потребляют записанные данные. Данные копируются непосредственно из записи в соответствующее чтение (или чтения); внутренней буферизации нет.
Безопасно вызывать Read и Write параллельно друг с другом или с Close. Параллельные вызовы Read и параллельные вызовы Write также безопасны: отдельные вызовы будут последовательно блокироваться.
Пример
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
r, w := io.Pipe()
go func() {
fmt.Fprint(w, "some io.Reader stream to be read\n")
w.Close()
}()
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
func ReadAll
func ReadAll(r Reader) ([]byte, error)
ReadAll читает из r до ошибки или EOF и возвращает прочитанные данные. Успешный вызов возвращает err == nil, а не err == EOF. Поскольку ReadAll определено для чтения из src до EOF, оно не рассматривает EOF из Read как ошибку, о которой следует сообщать.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")
b, err := io.ReadAll(r)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", b)
}
Output:
Go is a general-purpose language designed with systems programming in mind.
func ReadAtLeast
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
ReadAtLeast читает из r в buf, пока не прочитает как минимум min байт. Он возвращает количество скопированных байтов и ошибку, если было прочитано меньше байтов. Ошибка EOF возникает только в том случае, если не было прочитано ни одного байта. Если EOF возникает после чтения менее min байтов, ReadAtLeast возвращает ErrUnexpectedEOF. Если min больше длины buf, ReadAtLeast возвращает ErrShortBuffer. При возвращении n >= min, если и только если err == nil. Если r возвращает ошибку, прочитав не менее min байтов, ошибка игнорируется.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
buf := make([]byte, 14)
if _, err := io.ReadAtLeast(r, buf, 4); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// buffer smaller than minimal read size.
shortBuf := make([]byte, 3)
if _, err := io.ReadAtLeast(r, shortBuf, 4); err != nil {
fmt.Println("error:", err)
}
// minimal read size bigger than io.Reader stream
longBuf := make([]byte, 64)
if _, err := io.ReadAtLeast(r, longBuf, 64); err != nil {
fmt.Println("error:", err)
}
}
Output:
some io.Reader
error: short buffer
error: unexpected EOF
func ReadFull
func ReadFull(r Reader, buf []byte) (n int, err error)
ReadFull читает ровно len(buf) байтов из r в buf. Он возвращает количество скопированных байтов и ошибку, если было прочитано меньше байтов. Ошибка EOF возникает только в том случае, если не было прочитано ни одного байта. Если EOF возникает после чтения некоторых, но не всех байтов, ReadFull возвращает ErrUnexpectedEOF. При возвращении n == len(buf) тогда и только тогда, когда err == nil. Если r возвращает ошибку после чтения по крайней мере len(buf) байтов, ошибка игнорируется.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// minimal read size bigger than io.Reader stream
longBuf := make([]byte, 64)
if _, err := io.ReadFull(r, longBuf); err != nil {
fmt.Println("error:", err)
}
}
Output:
some
error: unexpected EOF
func WriteString
func WriteString(w Writer, s string) (n int, err error)
WriteString записывает содержимое строки s в w, который принимает набор байтов. Если w реализует StringWriter, [StringWriter.WriteString] вызывается напрямую. В противном случае [Writer.Write] вызывается ровно один раз.
Пример
package main
import (
"io"
"log"
"os"
)
func main() {
if _, err := io.WriteString(os.Stdout, "Hello World"); err != nil {
log.Fatal(err)
}
}
2 - Описание типов пакета io языка программирования GO
Описание типов и их функций пакета 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)
}
}
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!"
):
Прочитано 10 байт: ", 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)
}
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)
}
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)
}
}
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())
}
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!"))
}
Вывод:
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(...) // Ничего не делает
Используется для тестирования или игнорирования данных.
3 - Пакет для работы с файловой системой io/fs
“Пакет fs определяет базовые интерфейсы для файловой системы. Файловая система может быть предоставлена операционной системой хоста, а также другими пакетами.”
Для поддержки тестирования реализаций файловых систем смотрите пакет testing/fstest.
Переменные
var (
ErrInvalid = errInvalid() // "недопустимый аргумент"
ErrPermission = errPermission() // "разрешение отклонено"
ErrExist = errExist() // "файл уже существует"
ErrNotExist = errNotExist() // "файл не существует"
ErrClosed = errClosed() // "файл уже закрыт"
)
Общие ошибки файловой системы. Ошибки, возвращаемые файловыми системами, можно проверить на соответствие этим ошибкам с помощью errors.Is.
var SkipAll = errors.New("пропустить все и остановить прогулку")
SkipAll используется как возвращаемое значение из WalkDirFunc, чтобы указать, что все оставшиеся файлы и каталоги должны быть пропущены. Ни одна функция не возвращает его в качестве ошибки.
var SkipDir = errors.New("пропустить этот каталог")
SkipDir используется в качестве возвращаемого значения WalkDirFunc, чтобы указать, что каталог, названный в вызове, должен быть пропущен. Ни одна функция не возвращает его в качестве ошибки.
Функции
func FormatDirEntry
func FormatDirEntry(dir DirEntry) string
FormatDirEntry возвращает отформатированную версию dir для удобства чтения. Реализации DirEntry могут вызывать эту функцию из метода String. Результаты для каталога с именем subdir и файла с именем hello.go:
func FormatFileInfo(info FileInfo) string
FormatFileInfo возвращает отформатированную версию info для удобства чтения. Реализации FileInfo могут вызывать эту функцию из метода String. Результатом для файла с именем «hello.go», размером 100 байт, режимом 0o644, созданного 1 января 1970 года в полдень, будет
-rw-r--r-- 100 1970-01-01 12:00:00 hello.go
func Glob
func Glob(fsys FS, pattern string) (matches []string, err error)
Glob возвращает имена всех файлов, соответствующих pattern, или nil, если нет соответствующих файлов. Синтаксис шаблонов такой же, как в path.Match. Шаблон может описывать иерархические имена, такие как usr/*/bin/ed.
Glob игнорирует ошибки файловой системы, такие как ошибки ввода-вывода при чтении каталогов. Единственная возможная возвращаемая ошибка — path.ErrBadPattern, сообщающая о неверном формате шаблона.
Если fs реализует GlobFS, Glob вызывает fs.Glob. В противном случае Glob использует ReadDir для обхода дерева каталогов и поиска совпадений с шаблоном.
func ReadFile
func ReadFile(fsys FS, name string) ([]byte, error)
ReadFile читает файл с указанным именем из файловой системы fs и возвращает его содержимое. Успешный вызов возвращает ошибку nil, а не io.EOF. (Поскольку ReadFile читает весь файл, ожидаемый EOF от последнего Read не рассматривается как ошибка, о которой следует сообщать).
Если fs реализует ReadFileFS, ReadFile вызывает fs.ReadFile. В противном случае ReadFile вызывает fs.Open и использует Read и Close на возвращенном File.
func ValidPath
func ValidPath(name string) bool
ValidPath сообщает, является ли данное имя пути действительным для использования в вызове Open.
Имена путей, передаваемые в open, представляют собой закодированные в UTF-8, не имеющие корня, разделенные косой чертой последовательности элементов пути, например «x/y/z». Имена путей не должны содержать элемент «.» или «..» или пустую строку, за исключением особого случая, когда имя «.» может использоваться для корневого каталога. Пути не должны начинаться или заканчиваться косой чертой: «/x» и «x/» являются недопустимыми.
Обратите внимание, что пути разделяются косой чертой во всех системах, даже в Windows. Пути, содержащие другие символы, такие как обратная косая черта и двоеточие, принимаются как действительные, но эти символы никогда не должны интерпретироваться реализацией FS как разделители элементов пути.
func WalkDir
func WalkDir(fsys FS, root string, fn WalkDirFunc) error
WalkDir проходит по дереву файлов, корнем которого является root, вызывая fn для каждого файла или каталога в дереве, включая root.
Все ошибки, возникающие при посещении файлов и каталогов, фильтруются fn: подробности см. в документации fs.WalkDirFunc.
Файлы просматриваются в лексическом порядке, что делает вывод детерминированным, но требует, чтобы WalkDir считывал весь каталог в память, прежде чем приступать к просмотру этого каталога.
WalkDir не следует по символьным ссылкам, найденным в каталогах, но если root сам является символьной ссылкой, то будет пройдена его цель.
Пример
package main
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"time"
)
func main() {
// Создаем временную файловую систему для демонстрации
tmpDir, err := os.MkdirTemp("", "fs-example-*")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Создаем тестовые файлы и директории
createTestFS(tmpDir)
// Получаем файловую систему os.DirFS
fsys := os.DirFS(tmpDir)
// 1. Пример использования FormatDirEntry и FormatFileInfo
demoFormatFunctions(fsys)
// 2. Пример использования Glob
demoGlob(fsys)
// 3. Пример использования ReadFile
demoReadFile(fsys)
// 4. Пример использования ValidPath
demoValidPath()
// 5. Пример использования WalkDir
demoWalkDir(fsys)
}
func createTestFS(root string) {
// Создаем структуру:
// /tmp/fs-example-12345/
// ├── docs/
// │ ├── notes.md
// │ └── draft.txt
// ├── src/
// │ └── main.go
// └── README.txt
dirs := []string{
filepath.Join(root, "docs"),
filepath.Join(root, "src"),
}
files := map[string]string{
filepath.Join(root, "README.txt"): "Пример файла README",
filepath.Join(root, "docs", "notes.md"): "# Заметки\nПример содержимого",
filepath.Join(root, "docs", "draft.txt"): "Черновик документа",
filepath.Join(root, "src", "main.go"): "package main\n\nfunc main() {\n\tprintln(\"Hello\")\n}",
}
// Создаем директории
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatal(err)
}
}
// Создаем файлы
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatal(err)
}
}
// Устанавливаем время модификации для демонстрации FormatFileInfo
modTime := time.Date(2023, time.January, 15, 12, 30, 0, 0, time.UTC)
for path := range files {
if err := os.Chtimes(path, modTime, modTime); err != nil {
log.Fatal(err)
}
}
}
func demoFormatFunctions(fsys fs.FS) {
fmt.Println("\n=== FormatDirEntry и FormatFileInfo ===")
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
// FormatDirEntry
fmt.Printf("DirEntry: %s\n", fs.FormatDirEntry(entry))
// Получаем FileInfo для FormatFileInfo
info, err := entry.Info()
if err != nil {
log.Fatal(err)
}
fmt.Printf("FileInfo: %s\n", fs.FormatFileInfo(info))
}
}
func demoGlob(fsys fs.FS) {
fmt.Println("\n=== Glob ===")
// Ищем все .txt файлы
matches, err := fs.Glob(fsys, "*.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .txt в корне:", matches)
// Ищем все .go файлы в любых поддиректориях
matches, err = fs.Glob(fsys, "*/*.go")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .go в поддиректориях:", matches)
// Ищем все .md файлы рекурсивно
matches, err = fs.Glob(fsys, "**/*.md")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .md рекурсивно:", matches)
}
func demoReadFile(fsys fs.FS) {
fmt.Println("\n=== ReadFile ===")
// Читаем содержимое файла
content, err := fs.ReadFile(fsys, "src/main.go")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Содержимое main.go:\n%s\n", content)
// Попытка чтения несуществующего файла
_, err = fs.ReadFile(fsys, "nonexistent.txt")
fmt.Println("Ошибка при чтении несуществующего файла:", err)
}
func demoValidPath() {
fmt.Println("\n=== ValidPath ===")
paths := []string{
"valid/path",
"invalid/../path",
"invalid\\path", // Обратные слеши не считаются разделителями
"",
"trailing/slash/",
}
for _, path := range paths {
fmt.Printf("%q valid: %t\n", path, fs.ValidPath(path))
}
}
func demoWalkDir(fsys fs.FS) {
fmt.Println("\n=== WalkDir ===")
fmt.Println("Содержимое файловой системы:")
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf("- %s\n", fs.FormatDirEntry(d))
return nil
})
if err != nil {
log.Fatal(err)
}
}
=== FormatDirEntry и FormatFileInfo ===
DirEntry: d docs/
FileInfo: drwxr-xr-x 4096 2023-01-15 12:30:00 docs
DirEntry: d src/
FileInfo: drwxr-xr-x 4096 2023-01-15 12:30:00 src
DirEntry: - README.txt
FileInfo: -rw-r--r-- 19 2023-01-15 12:30:00 README.txt
=== Glob ===
Файлы .txt в корне: [README.txt]
Файлы .go в поддиректориях: [src/main.go]
Файлы .md рекурсивно: [docs/notes.md]
=== ReadFile ===
Содержимое main.go:
package main
func main() {
println("Hello")
}
Ошибка при чтении несуществующего файла: open nonexistent.txt: file does not exist
=== ValidPath ===
"valid/path" valid: true
"invalid/../path" valid: false
"invalid\\path" valid: true
"" valid: false
"trailing/slash/" valid: false
=== WalkDir ===
Содержимое файловой системы:
- .
- d docs/
- - docs/draft.txt
- - docs/notes.md
- d src/
- - src/main.go
- - README.txt
Типы
type DirEntry
type DirEntry interface {
// Name возвращает имя файла (или подкаталога), описанного записью.
// Это имя является только конечным элементом пути (базовым именем), а не всем путем.
// Например, Name вернет «hello.go», а не «home/gopher/hello.go».
Name() string
// IsDir сообщает, описывает ли запись каталог.
IsDir() bool
// Type возвращает биты типа для запис
// Биты типа являются подмножеством обычных битов FileMode, возвращаемых методом FileMode.Type.
Type() FileMode
// Info возвращает FileInfo для файла или подкаталога, описанного записью.
// Возвращаемая информация FileInfo может быть взята из времени первоначального чтения каталога
// или из времени вызова Info. Если файл был удален или переименован
// после чтения каталога, Info может вернуть ошибку, удовлетворяющую errors.Is(err, ErrNotExist).
// Если запись обозначает символическую ссылку, Info сообщает информацию о самой ссылке,
// а не о цели ссылки.
Info() (FileInfo, error)
}
DirEntry — это запись, прочитанная из каталога (с помощью функции ReadDir или метода ReadDir класса ReadDirFile).
func FileInfoToDirEntry
func FileInfoToDirEntry(info FileInfo) DirEntry
FileInfoToDirEntry возвращает DirEntry, который возвращает информацию из info. Если info равно nil, FileInfoToDirEntry возвращает nil.
func ReadDir
func ReadDir(fsys FS, name string) ([]DirEntry, error)
ReadDir считывает указанный каталог и возвращает список записей каталога, отсортированных по имени файла.
Если fs реализует ReadDirFS, ReadDir вызывает fs.ReadDir. В противном случае ReadDir вызывает fs.Open и использует ReadDir и Close для возвращенного файла.
type FS
type FS interface {
// Open открывает указанный файл.
// [File.Close] должен быть вызван для освобождения всех связанных ресурсов.
//
// Когда Open возвращает ошибку, она должна быть типа *PathError
// с полем Op, установленным в «open», полем Path, установленным в name,
// и полем Err, описывающим проблему.
//
// Open должен отклонять попытки открыть имена, которые не удовлетворяют
// ValidPath(name), возвращая *PathError с Err, установленным в
// ErrInvalid или ErrNotExist.
Open(name string) (File, error)
}
FS обеспечивает доступ к иерархической файловой системе.
Интерфейс FS является минимальной реализацией, необходимой для файловой системы. Файловая система может реализовывать дополнительные интерфейсы, такие как ReadFileFS, для предоставления дополнительных или оптимизированных функций.
testing/fstest.TestFS может использоваться для проверки правильности реализации FS.
Объяснение FS
Объяснение интерфейса fs.FS
fs.FS
- это базовый интерфейс в Go для работы с иерархическими файловыми системами. Он представляет минимальный контракт, который должна реализовать любая файловая система (реальная, виртуальная, в памяти и т.д.).
Ключевые особенности:
- Минималистичный дизайн - только один обязательный метод
Open()
- Абстракция - позволяет работать с разными ФС одинаковым способом
- Расширяемость - через дополнительные интерфейсы (
ReadFileFS
, GlobFS
и др.)
Основной метод:
Open(name string) (File, error)
- Открывает файл по имени
- Возвращает объект, реализующий
fs.File
- В случае ошибки возвращает
*fs.PathError
Пример реализации и использования
1. Создаем простую in-memory файловую систему
package main
import (
"io/fs"
"log"
"os"
"time"
)
// MemoryFS - простая in-memory реализация fs.FS
type MemoryFS struct {
files map[string]*MemoryFile
}
type MemoryFile struct {
name string
content []byte
mode fs.FileMode
modTime time.Time
}
func (m *MemoryFile) Stat() (fs.FileInfo, error) {
return &MemoryFileInfo{m}, nil
}
func (m *MemoryFile) Read(p []byte) (int, error) {
// Реализация чтения
}
func (m *MemoryFile) Close() error {
return nil
}
// MemoryFileInfo реализует fs.FileInfo
type MemoryFileInfo struct {
file *MemoryFile
}
func (m *MemoryFileInfo) Name() string { return m.file.name }
func (m *MemoryFileInfo) Size() int64 { return int64(len(m.file.content)) }
func (m *MemoryFileInfo) Mode() fs.FileMode { return m.file.mode }
func (m *MemoryFileInfo) ModTime() time.Time { return m.file.modTime }
func (m *MemoryFileInfo) IsDir() bool { return m.file.mode.IsDir() }
func (m *MemoryFileInfo) Sys() interface{} { return nil }
// Open реализует fs.FS
func (mfs *MemoryFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
file, exists := mfs.files[name]
if !exists {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
return file, nil
}
2. Используем нашу реализацию
func main() {
// Инициализируем нашу ФС
mfs := &MemoryFS{
files: map[string]*MemoryFile{
"hello.txt": {
name: "hello.txt",
content: []byte("Hello, MemoryFS!"),
mode: 0644,
modTime: time.Now(),
},
"dir": {
name: "dir",
mode: fs.ModeDir | 0755,
modTime: time.Now(),
},
},
}
// Пример 1: Открываем и читаем файл
file, err := mfs.Open("hello.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
info, _ := file.Stat()
fmt.Printf("Файл: %s, размер: %d\n", info.Name(), info.Size())
// Пример 2: Пытаемся открыть несуществующий файл
_, err = mfs.Open("missing.txt")
if err != nil {
fmt.Printf("Ошибка: %v\n", err) // Выведет PathError
}
// Пример 3: Используем с другими функциями пакета fs
content, err := fs.ReadFile(mfs, "hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Содержимое: %s\n", content)
}
3. Пример вывода:
Файл: hello.txt, размер: 15
Ошибка: open missing.txt: file does not exist
Содержимое: Hello, MemoryFS!
Где используется fs.FS?
-
В стандартной библиотеке:
os.DirFS
- доступ к реальной файловой системе
embed.FS
- доступ к встроенным файлам
http.FileSystem
- интеграция с веб-сервером
-
В популярных библиотеках:
- Виртуальные файловые системы
- Работа с архивами (zip, tar)
- Тестирование (testing/fstest)
-
В пользовательских реализациях:
- ФС в памяти
- ФС поверх облачного хранилища
- ФС для специализированных форматов
Преимущества такого подхода:
- Единообразие - один интерфейс для разных ФС
- Тестируемость - легко подменять реальную ФС на mock
- Гибкость - можно комбинировать разные реализации
Интерфейс fs.FS
стал стандартным способом работы с файловыми системами в Go 1.16+, заменив множество специализированных решений.
func Sub
func Sub(fsys FS, dir string) (FS, error)
Sub возвращает FS, соответствующую поддереву с корнем в fsys’s dir.
Если dir равна «.», Sub возвращает fsys без изменений. В противном случае, если fs реализует SubFS, Sub возвращает fsys.Sub(dir). В противном случае Sub возвращает новую реализацию FS sub, которая фактически реализует sub.Open(name) как fsys.Open(path.Join(dir, name)). Реализация также соответствующим образом преобразует вызовы ReadDir, ReadFile и Glob.
Обратите внимание, что Sub(os.DirFS(«/»), „prefix“) эквивалентно os.DirFS(«/prefix») и что ни одно из них не гарантирует отсутствие доступа операционной системы за пределами «/prefix», поскольку реализация os.DirFS не проверяет символьные ссылки внутри «/prefix», которые указывают на другие каталоги. То есть os.DirFS не является общей заменой механизма безопасности типа chroot, и Sub не меняет этот факт.
type File
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
File предоставляет доступ к одному файлу. Интерфейс File является минимальной реализацией, требуемой для файла. Файлы каталогов также должны реализовывать ReadDirFile. Файл может реализовывать io.ReaderAt или io.Seeker в качестве оптимизаций.
type FileInfo
type FileInfo interface {
Name() string // базовое имя файла
Size() int64 // длина в байтах для обычных файлов; зависит от системы для других
Mode() FileMode // биты режима файла
ModTime() time.Time // время изменения
IsDir() bool // сокращение для Mode().IsDir()
Sys() any // базовый источник данных (может возвращать nil)
}
FileInfo описывает файл и возвращается Stat.
func Stat
func Stat(fsys FS, name string) (FileInfo, error)
Stat возвращает FileInfo, описывающий файл с указанным именем из файловой системы.
Если fs реализует StatFS, Stat вызывает fs.Stat. В противном случае Stat открывает файл для его статистики.
type FileMode
FileMode представляет режим файла и биты разрешений. Биты имеют одинаковое определение во всех системах, поэтому информация о файлах может быть перенесена из одной системы в другую. Не все биты применимы ко всем системам. Единственный обязательный бит — ModeDir для каталогов.
const (
// Одиночные буквы являются аббревиатурами,
// используемыми для форматирования методом String.
ModeDir FileMode = 1 << (32 - 1 - iota) // d: является каталогом
ModeAppend // a: только для добавления
ModeExclusive // l: исключительное использование
ModeTemporary // T: временный файл; только Plan 9
ModeSymlink // L: символическая ссылка
ModeDevice // D: файл устройства
ModeNamedPipe // p: именованный канал (FIFO)
ModeSocket // S: сокет домена Unix
ModeSetuid // u: setuid
ModeSetgid // g: setgid
ModeCharDevice // c: символьное устройство Unix, когда установлен ModeDevice
ModeSticky // t: sticky
ModeIrregular // ?: нерегулярный файл; ничего больше не известно об этом файле
// Маска для битов типа. Для обычных файлов не будет установлено ничего.
ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular
ModePerm FileMode = 0777 // биты разрешений Unix)
Определенные биты режима файла являются наиболее значимыми битами FileMode. Девять наименее значимых битов являются стандартными разрешениями Unix rwxrwxrwx. Значения этих битов следует рассматривать как часть общедоступного API и могут использоваться в протоколах передачи данных или представлениях диска: они не должны изменяться, хотя могут быть добавлены новые биты.
func (FileMode) IsDir
func (m FileMode) IsDir() bool
IsDir сообщает, описывает ли m каталог. То есть, он проверяет, установлен ли бит ModeDir в m.
func (FileMode) IsRegular
func (m FileMode) IsRegular() bool
IsRegular сообщает, описывает ли m обычный файл. То есть, он проверяет, что биты типа режима не установлены.
func (FileMode) Perm
func (m FileMode) Perm() FileMode
Perm возвращает биты разрешений Unix в m (m & ModePerm).
func (FileMode) String
func (m FileMode) String() string
func (FileMode) Type
func (m FileMode) Type() FileMode
Type возвращает биты типа в m (m & ModeType).
type GlobFS
type GlobFS interface {
FS
// Glob возвращает имена всех файлов, соответствующих шаблону,
// предоставляя реализацию функции верхнего уровня
// Glob.
Glob(pattern string) ([]string, error)
}
GlobFS — это файловая система с методом Glob.
type PathError
type PathError struct {
Op string
Path string
Err error
}
PathError регистрирует ошибку, а также операцию и путь к файлу, которые ее вызвали.
func (*PathError) Error
func (e *PathError) Error() string
func (*PathError) Timeout
func (e *PathError) Timeout() bool
Timeout сообщает, является ли эта ошибка тайм-аутом.
func (*PathError) Unwrap
func (e *PathError) Unwrap() error
type ReadDirFS
type ReadDirFS interface {
FS
// ReadDir читает указанный каталог
// и возвращает список записей каталога, отсортированных по имени файла.
ReadDir(name string) ([]DirEntry, error)
}
ReadDirFS — это интерфейс, реализованный файловой системой, который обеспечивает оптимизированную реализацию ReadDir.
type ReadDirFile
type ReadDirFile interface {
file
// ReadDir считывает содержимое каталога и возвращает
// массив из n значений DirEntry в порядке каталога.
// Последующие вызовы для того же файла будут возвращать дальнейшие значения DirEntry.
//
// Если n > 0, ReadDir возвращает не более n структур DirEntry.
// В этом случае, если ReadDir возвращает пустой срез, он вернет
// не нулевую ошибку с объяснением причины.
// В конце каталога ошибкой будет io.EOF.
// (ReadDir должен возвращать io.EOF сам, а не ошибку, оборачивающую io.EOF.)
//
// Если n <= 0, ReadDir возвращает все значения DirEntry из каталога
// в одном срезе. В этом случае, если ReadDir завершается успешно (считывает все
// до конца каталога), он возвращает срез и ошибку nil.
// Если он встречает ошибку до конца каталога,
// ReadDir возвращает список DirEntry, прочитанный до этого момента, и ошибку, отличную от nil.
ReadDir(n int) ([]DirEntry, error)
}
ReadDirFile — это файл каталога, записи которого можно прочитать с помощью метода ReadDir. Каждый файл каталога должен реализовывать этот интерфейс. (Любой файл может реализовывать этот интерфейс, но в этом случае ReadDir должен возвращать ошибку для некаталогов.)
type ReadFileFS
type ReadFileFS interface {
FS
// ReadFile читает указанный файл и возвращает его содержимое.
// Успешный вызов возвращает ошибку nil, а не io.EOF.
// (Поскольку ReadFile считывает весь файл, ожидаемый EOF
// от последнего Read не рассматривается как ошибка, о которой следует сообщать.)
//
// Вызывающему разрешается изменять возвращаемый байтовый срез.
// Этот метод должен возвращать копию базовых данных.
ReadFile(name string) ([]byte, error)
}
ReadFileFS — это интерфейс, реализованный файловой системой, который обеспечивает оптимизированную реализацию ReadFile.
type StatFS
type StatFS interface {
FS
// Stat возвращает FileInfo, описывающий файл.
// Если происходит ошибка, она должна быть типа *PathError.
Stat(name string) (FileInfo, error)
}
StatFS — это файловая система с методом Stat.
type SubFS
type SubFS interface {
FS
// Sub возвращает FS, соответствующий поддереву с корнем в dir.
Sub(dir string) (FS, error)
}
SubFS — это файловая система с методом Sub.
type WalkDirFunc
type WalkDirFunc func(path string, d DirEntry, err error) error
WalkDirFunc — это тип функции, вызываемой WalkDir для посещения каждого файла или каталога.
Аргумент path содержит аргумент WalkDir в качестве префикса. То есть, если WalkDir вызывается с корневым аргументом «dir» и находит файл с именем «a» в этом каталоге, функция walk будет вызвана с аргументом «dir/a».
Аргумент d — это DirEntry для указанного пути.
Результат error, возвращаемый функцией, контролирует продолжение работы WalkDir. Если функция возвращает специальное значение SkipDir, WalkDir пропускает текущий каталог (path, если d.IsDir() равно true, в противном случае — родительский каталог path). Если функция возвращает специальное значение SkipAll, WalkDir пропускает все оставшиеся файлы и каталоги. В противном случае, если функция возвращает ошибку, отличную от nil, WalkDir полностью останавливается и возвращает эту ошибку.
Аргумент err сообщает об ошибке, связанной с путем, сигнализируя, что WalkDir не будет проходить в этот каталог. Функция может решить, как обработать эту ошибку; как описано ранее, возврат ошибки приведет к тому, что WalkDir прекратит прохождение всего дерева.
WalkDir вызывает функцию с аргументом err, отличным от nil, в двух случаях.
Во-первых, если первоначальная Stat в корневом каталоге завершается с ошибкой, WalkDir вызывает функцию с path, установленным в root, d, установленным в nil, и err, установленным в ошибку из fs.Stat.
Во-вторых, если метод ReadDir каталога (см. ReadDirFile) завершается с ошибкой, WalkDir вызывает функцию с path, установленным в путь каталога, d, установленным в DirEntry, описывающий каталог, и err, установленным в ошибку из ReadDir. Во втором случае функция вызывается дважды с путем к каталогу: первый вызов происходит до попытки чтения каталога, и err установлен в nil, что дает функции возможность вернуть SkipDir или SkipAll и полностью избежать ReadDir. Второй вызов происходит после неудачного ReadDir и сообщает об ошибке из ReadDir. (Если ReadDir завершается успешно, второго вызова не происходит.)
Различия между WalkDirFunc и path/filepath.WalkFunc заключаются в следующем:
Второй аргумент имеет тип DirEntry вместо FileInfo.
Функция вызывается перед чтением каталога, чтобы SkipDir или SkipAll могли полностью обойти чтение каталога или пропустить все оставшиеся файлы и каталоги соответственно.
Если чтение каталога завершилось неудачно, функция вызывается второй раз для этого каталога, чтобы сообщить об ошибке.