Работа с файлами

Работа с файлами - одна из базовых задач бэкенда: прочитать конфиг, записать лог, обработать CSV-выгрузку. В Go всё это делается через стандартную библиотеку, без сторонних зависимостей. Про возврат и проверку ошибок - урок Обработка ошибок, про defer - урок Функции и несколько возвращаемых значений.

Чтение и запись целиком

Если файл небольшой (конфиг, JSON, шаблон), проще всего прочитать его целиком в память. Пакет os предоставляет для этого две функции:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Чтение файла целиком в []byte
    content, err := os.ReadFile("config.json")
    if err != nil {
        fmt.Println("Ошибка чтения:", err)
        return
    }
    fmt.Println(string(content))

    // Запись в файл (создаст или перезапишет)
    // 0644 - владелец читает и пишет, остальные только читают
    err = os.WriteFile("output.txt", []byte("Hello, Go!"), 0644)
    if err != nil {
        fmt.Println("Ошибка записи:", err)
    }
}

os.ReadFile загружает весь файл в память. Для маленьких файлов (до нескольких мегабайт) это нормально. Для больших - нет.

Проверка существования файла

Перед чтением иногда нужно узнать, существует ли файл:

package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    _, err := os.Stat("config.json")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("Файл не существует")
        return
    }
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Println("Файл найден")
}

Важно: не проверяй существование файла перед открытием. Между проверкой и открытием файл может исчезнуть (race condition). Лучше сразу открывать и обрабатывать ошибку.

Открытие файла: os.Open и os.Create

Когда нужен больший контроль - построчное чтение, запись порциями - используется os.Open (чтение) и os.Create (запись):

package main

import (
    "fmt"
    "os"
)

func main() {
    // Открытие для чтения
    file, err := os.Open("data.txt")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer file.Close() // Всегда закрывайте файл!

    // Чтение в буфер
    buf := make([]byte, 1024)
    n, err := file.Read(buf)
    if err != nil {
        fmt.Println("Ошибка чтения:", err)
        return
    }
    fmt.Printf("Прочитано %d байт: %s\n", n, buf[:n])
}

defer file.Close() - обязательный паттерн. Без него файловый дескриптор утечёт, и при большом количестве открытий система откажет в доступе.

Построчное чтение: bufio.Scanner

Самый частый сценарий - обработка логов, CSV, текстовых файлов построчно. Для этого есть bufio.Scanner (с ним мы уже встречались в уроке Получение данных от пользователя):

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("server.log")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNum := 0

    for scanner.Scan() {
        lineNum++
        line := scanner.Text()
        fmt.Printf("%d: %s\n", lineNum, line)
    }

    // Обязательно проверяем ошибку после цикла
    if err := scanner.Err(); err != nil {
        fmt.Println("Ошибка при чтении:", err)
    }
}

По умолчанию Scanner ограничен строками до 64 КБ. Если строки длиннее (JSON-логи, например), увеличьте буфер:

scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // До 1 МБ на строку

Потоковое копирование: io.Copy

Для больших бинарных файлов (видео, архивы, бэкапы) нельзя грузить всё в память. io.Copy читает и пишет порциями:

package main

import (
    "fmt"
    "io"
    "os"
)

func copyFile(src, dst string) error {
    source, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("открытие источника: %w", err)
    }
    defer source.Close()

    destination, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("создание файла: %w", err)
    }
    defer destination.Close()

    bytes, err := io.Copy(destination, source)
    if err != nil {
        return fmt.Errorf("копирование: %w", err)
    }

    fmt.Printf("Скопировано %d байт\n", bytes)
    return nil
}

func main() {
    if err := copyFile("backup.tar.gz", "backup_copy.tar.gz"); err != nil {
        fmt.Println("Ошибка:", err)
    }
}

io.Copy использует внутренний буфер 32 КБ. Файл любого размера копируется без проблем с памятью.

Запись с буферизацией: bufio.Writer

Если нужно записать много маленьких порций (построчная генерация файла), bufio.Writer накапливает данные и пишет на диск большими блоками:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("report.csv")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer file.Close()

    writer := bufio.NewWriter(file)

    // Заголовок
    fmt.Fprintln(writer, "id,name,score")

    // Данные
    for i := 1; i <= 1000; i++ {
        fmt.Fprintf(writer, "%d,user_%d,%d\n", i, i, i*10)
    }

    // Обязательно сбрасываем буфер на диск!
    if err := writer.Flush(); err != nil {
        fmt.Println("Ошибка записи:", err)
    }
}

Не забывайте Flush()! Без него последняя порция данных останется в буфере и не попадёт в файл.

Работа с директориями

package main

import (
    "fmt"
    "os"
)

func main() {
    // Создание директории (с вложенными)
    err := os.MkdirAll("data/logs/2024", 0755)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    // Чтение содержимого директории
    entries, err := os.ReadDir("data")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    for _, entry := range entries {
        if entry.IsDir() {
            fmt.Printf("[DIR]  %s\n", entry.Name())
        } else {
            fmt.Printf("[FILE] %s\n", entry.Name())
        }
    }
}

Временные файлы

Для промежуточных данных используйте os.CreateTemp - Go сам выберет уникальное имя:

package main

import (
    "fmt"
    "os"
)

func main() {
    // "" - использовать системную temp-директорию
    tmpFile, err := os.CreateTemp("", "report-*.csv")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer os.Remove(tmpFile.Name()) // Удаляем после использования
    defer tmpFile.Close()

    fmt.Fprintln(tmpFile, "id,value")
    fmt.Fprintln(tmpFile, "1,42")

    fmt.Println("Временный файл:", tmpFile.Name())
}

Типичные ошибки

  1. Забыть defer file.Close() - утечка дескрипторов. При 1000+ открытий система откажет.
  2. Забыть writer.Flush() - потеря данных в конце файла.
  3. Читать огромный файл через os.ReadFile - OOM (out of memory). Используйте bufio.Scanner или io.Copy.
  4. Не проверять scanner.Err() - ошибки чтения проглатываются молча.
  5. Проверять файл перед открытием - race condition. Открывайте сразу и обрабатывайте ошибку.

Что запомнить

  • os.ReadFile / os.WriteFile - для маленьких файлов (конфиги, JSON)
  • bufio.Scanner - для построчного чтения (логи, CSV)
  • io.Copy - для больших бинарных файлов (потоково, без загрузки в память)
  • bufio.Writer - для эффективной построчной записи
  • defer file.Close() - всегда
  • os.CreateTemp - для временных файлов

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.