MidТеория9 min

Ввод/вывод: io, os, fs

Интерфейсы io.Reader/Writer, работа с файлами, embed, fs и bufio

Система ввода-вывода Go построена на двух фундаментальных интерфейсах: io.Reader и io.Writer. Вся экосистема -- от файлов до сети, от сжатия до шифрования -- использует эти интерфейсы. Понимание io -- ключ к написанию эффективного и компонуемого кода.

io пакет: фундаментальные интерфейсы

io.Reader и io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Это самые важные интерфейсы в Go. Их реализуют:

  • *os.File -- файлы
  • *bytes.Buffer -- буфер в памяти
  • *strings.Reader -- чтение из строки
  • net.Conn -- сетевые соединения
  • *http.Request.Body -- тело HTTP-запроса
  • *gzip.Writer -- сжатие
  • *cipher.StreamWriter -- шифрование

io.Closer и io.Seeker

type Closer interface {
    Close() error
}

type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// Composed interfaces
type ReadCloser interface {
    Reader
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

io.Copy -- универсальное копирование

// Copy from reader to writer -- the most used io function
func copyFile(dst, src string) error {
    in, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("opening source: %w", err)
    }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("creating destination: %w", err)
    }
    defer out.Close()

    // io.Copy uses a 32KB buffer internally
    written, err := io.Copy(out, in)
    if err != nil {
        return fmt.Errorf("copying: %w", err)
    }

    fmt.Printf("Copied %d bytes\n", written)
    return out.Close() // Important: check close error for writes!
}

io.ReadAll -- прочитать всё

func readResponse(resp *http.Response) ([]byte, error) {
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("reading response: %w", err)
    }

    return data, nil
}

Осторожно: io.ReadAll загружает весь контент в память. Для больших файлов используйте потоковую обработку через io.Copy или bufio.Scanner.

io.TeeReader -- читать и копировать одновременно

// TeeReader: everything read from r is also written to w
func logAndProcess(r io.Reader) error {
    var buf bytes.Buffer
    tee := io.TeeReader(r, &buf)

    // Process the data (reads go to both processor and buf)
    if err := process(tee); err != nil {
        return err
    }

    // buf now contains everything that was read
    log.Printf("Processed data: %s", buf.String())
    return nil
}

io.Pipe -- синхронный поток

func streamJSON(users []User) io.Reader {
    pr, pw := io.Pipe()

    go func() {
        enc := json.NewEncoder(pw)
        for _, u := range users {
            if err := enc.Encode(u); err != nil {
                pw.CloseWithError(err)
                return
            }
        }
        pw.Close()
    }()

    return pr // Reader that yields JSON as it's encoded
}

io.MultiReader и io.MultiWriter

// MultiReader: concatenate multiple readers
func concatenateFiles(files ...string) io.Reader {
    readers := make([]io.Reader, 0, len(files)*2)
    for i, f := range files {
        file, _ := os.Open(f)
        readers = append(readers, file)
        if i < len(files)-1 {
            readers = append(readers, strings.NewReader("\n---\n"))
        }
    }
    return io.MultiReader(readers...)
}

// MultiWriter: write to multiple destinations at once
func loggedWriter(w io.Writer) io.Writer {
    logFile, _ := os.Create("output.log")
    return io.MultiWriter(w, logFile) // Writes go to both
}

io.LimitReader -- ограничение чтения

// Prevent reading more than 1MB
func safeRead(r io.Reader) ([]byte, error) {
    const maxSize = 1 << 20 // 1 MB
    limited := io.LimitReader(r, maxSize)
    return io.ReadAll(limited)
}

os пакет: файлы и окружение

Открытие и создание файлов

// Read-only open
f, err := os.Open("data.txt") // Same as os.OpenFile("data.txt", os.O_RDONLY, 0)
if err != nil {
    return fmt.Errorf("opening file: %w", err)
}
defer f.Close()

// Create (truncate if exists, create if not)
f, err := os.Create("output.txt") // Same as os.OpenFile("output.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
    return fmt.Errorf("creating file: %w", err)
}
defer f.Close()

// OpenFile with specific flags
f, err := os.OpenFile("app.log",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, // Append, create if needed, write-only
    0644,                                  // rw-r--r--
)
if err != nil {
    return fmt.Errorf("opening log file: %w", err)
}
defer f.Close()

os.ReadFile и os.WriteFile (Go 1.16+)

Удобные функции для простых операций:

// Read entire file into memory
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("reading config: %w", err)
}

// Write data to file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
    return fmt.Errorf("writing file: %w", err)
}

Права доступа (file permissions)

// Common permission patterns:
// 0644 -- owner: rw, group: r, others: r  (files)
// 0755 -- owner: rwx, group: rx, others: rx (directories, executables)
// 0600 -- owner: rw only (secrets, keys)
// 0700 -- owner: rwx only (private directories)

os.WriteFile("public.txt", data, 0644)
os.WriteFile("secret.key", key, 0600)
os.Mkdir("mydir", 0755)
os.MkdirAll("path/to/deep/dir", 0755)

os.Stat -- информация о файле

info, err := os.Stat("myfile.txt")
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File does not exist")
    return
}
if err != nil {
    return fmt.Errorf("stat: %w", err)
}

fmt.Printf("Name: %s\n", info.Name())
fmt.Printf("Size: %d bytes\n", info.Size())
fmt.Printf("Mode: %s\n", info.Mode())
fmt.Printf("Modified: %s\n", info.ModTime())
fmt.Printf("IsDir: %v\n", info.IsDir())

Переменные окружения

// Get environment variable
dbHost := os.Getenv("DB_HOST")
if dbHost == "" {
    dbHost = "localhost" // Default
}

// Get with existence check
port, ok := os.LookupEnv("PORT")
if !ok {
    port = "8080"
}

// Set environment variable
os.Setenv("APP_ENV", "test")

// All environment variables
for _, env := range os.Environ() {
    // env is "KEY=VALUE"
    parts := strings.SplitN(env, "=", 2)
    fmt.Printf("%s = %s\n", parts[0], parts[1])
}

os.Args -- аргументы командной строки

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s <filename>\n", os.Args[0])
        os.Exit(1)
    }

    filename := os.Args[1]
    // Process file...
}

filepath пакет: работа с путями

import "path/filepath"

// Join path components (handles separators correctly)
path := filepath.Join("home", "user", "documents", "file.txt")
// Linux: "home/user/documents/file.txt"
// Windows: "home\\user\\documents\\file.txt"

// Get directory and filename
dir := filepath.Dir("/home/user/file.txt")    // "/home/user"
base := filepath.Base("/home/user/file.txt")   // "file.txt"
ext := filepath.Ext("archive.tar.gz")          // ".gz"

// Clean path (resolve .., . etc)
clean := filepath.Clean("/home/user/../admin/./file.txt")
// "/home/admin/file.txt"

// Absolute path
abs, err := filepath.Abs("relative/path")

// Match pattern
matched, err := filepath.Match("*.go", "main.go") // true

filepath.WalkDir -- обход дерева каталогов

// WalkDir is more efficient than Walk (Go 1.16+)
func findGoFiles(root string) ([]string, error) {
    var files []string

    err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err // Propagate errors
        }

        // Skip hidden directories
        if d.IsDir() && strings.HasPrefix(d.Name(), ".") {
            return filepath.SkipDir
        }

        // Collect .go files
        if !d.IsDir() && filepath.Ext(path) == ".go" {
            files = append(files, path)
        }

        return nil
    })

    return files, err
}

embed пакет (Go 1.16+)

Директива //go:embed встраивает файлы прямо в бинарник при компиляции:

import "embed"

// Embed single file as string
//go:embed version.txt
var version string

// Embed single file as bytes
//go:embed config/default.json
var defaultConfig []byte

// Embed directory as filesystem
//go:embed templates/*
var templatesFS embed.FS

// Embed multiple patterns
//go:embed static/*.css static/*.js static/*.html
var staticFiles embed.FS

func main() {
    fmt.Println("Version:", version)

    // Read from embedded FS
    data, err := templatesFS.ReadFile("templates/index.html")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))

    // List embedded files
    entries, _ := templatesFS.ReadDir("templates")
    for _, e := range entries {
        fmt.Println(e.Name())
    }
}

Embed с HTTP-сервером

//go:embed static
var staticFS embed.FS

func main() {
    // Serve embedded static files
    staticHandler := http.FileServer(http.FS(staticFS))
    http.Handle("/static/", staticHandler)

    // Or strip prefix
    sub, _ := fs.Sub(staticFS, "static")
    http.Handle("/", http.FileServer(http.FS(sub)))

    http.ListenAndServe(":8080", nil)
}

io/fs пакет: абстракция файловой системы

Go 1.16 добавил интерфейс fs.FS -- абстракцию над файловой системой:

type FS interface {
    Open(name string) (File, error)
}

type File interface {
    Stat() (FileInfo, error)
    Read([]byte) (int, error)
    Close() error
}

fs.WalkDir и fs.Sub

// Works with any fs.FS: os filesystem, embed.FS, zip files, etc.
func countFiles(fsys fs.FS) (int, error) {
    count := 0
    err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() {
            count++
        }
        return nil
    })
    return count, err
}

// fs.Sub creates a sub-filesystem
func getTemplates(fsys fs.FS) (fs.FS, error) {
    return fs.Sub(fsys, "templates")
}

Тестирование с fs.FS

import "testing/fstest"

func TestCountFiles(t *testing.T) {
    // Create in-memory filesystem for testing
    testFS := fstest.MapFS{
        "file1.txt":        {Data: []byte("hello")},
        "file2.txt":        {Data: []byte("world")},
        "subdir/file3.txt": {Data: []byte("nested")},
    }

    count, err := countFiles(testFS)
    if err != nil {
        t.Fatal(err)
    }
    if count != 3 {
        t.Errorf("count = %d, want 3", count)
    }
}

bufio: буферизированный I/O

bufio.Scanner -- чтение по строкам

func countLines(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count, scanner.Err()
}

// Read file line by line
func processLines(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("opening file: %w", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)

    // Increase buffer for long lines (default 64KB max)
    buf := make([]byte, 0, 1024*1024)
    scanner.Buffer(buf, 10*1024*1024) // Max 10MB per line

    lineNum := 0
    for scanner.Scan() {
        lineNum++
        line := scanner.Text() // or scanner.Bytes() for []byte
        fmt.Printf("%d: %s\n", lineNum, line)
    }

    return scanner.Err()
}

Пользовательский split-функция

// Split by words (built-in)
scanner.Split(bufio.ScanWords)

// Split by runes
scanner.Split(bufio.ScanRunes)

// Custom: split by paragraphs (double newline)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    if atEOF && len(data) == 0 {
        return 0, nil, nil
    }
    if i := bytes.Index(data, []byte("\n\n")); i >= 0 {
        return i + 2, data[:i], nil
    }
    if atEOF {
        return len(data), data, nil
    }
    return 0, nil, nil // Need more data
})

bufio.Reader и bufio.Writer

// Buffered reader -- reduces system calls
br := bufio.NewReader(file)
br := bufio.NewReaderSize(file, 64*1024) // Custom buffer size

line, err := br.ReadString('\n') // Read until delimiter
b, err := br.ReadByte()          // Read single byte
peek, err := br.Peek(10)         // Peek without consuming

// Buffered writer -- batches writes
bw := bufio.NewWriter(file)
bw.WriteString("Hello, ")
bw.WriteString("World!\n")
bw.Flush() // Don't forget to flush!

// Or use defer
bw := bufio.NewWriter(file)
defer bw.Flush()

Практический пример: обработка CSV с потоками

func processLargeCSV(inputPath, outputPath string) error {
    // Open input
    in, err := os.Open(inputPath)
    if err != nil {
        return fmt.Errorf("opening input: %w", err)
    }
    defer in.Close()

    // Create output
    out, err := os.Create(outputPath)
    if err != nil {
        return fmt.Errorf("creating output: %w", err)
    }
    defer out.Close()

    // Buffered I/O for performance
    reader := csv.NewReader(bufio.NewReader(in))
    writer := csv.NewWriter(bufio.NewWriter(out))
    defer writer.Flush()

    // Process line by line -- constant memory usage
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("reading CSV: %w", err)
        }

        // Transform record
        transformed := transformRecord(record)

        if err := writer.Write(transformed); err != nil {
            return fmt.Errorf("writing CSV: %w", err)
        }
    }

    return nil
}

Проверь себя

Почему WalkDir предпочтительнее Walk?

Что делает io.TeeReader?

Зачем нужен bufio.Scanner.Buffer() с увеличенным буфером?

Что делает директива //go:embed?

Какие два фундаментальных интерфейса лежат в основе системы ввода-вывода Go?