Go и файлы: от открытия до потоковой обработки данных

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему работа с файлами в Go — это не страшно, а интересно?

Когда я впервые открыл документацию пакета os в Go, мне показалось, что меня ждет скучное перечисление системных вызовов. Но, покопавшись глубже, я понял: работа с файлами и файловой системой в Go — это не просто чтение байтов. Это мощный инструмент, который дает вам полный контроль над данными, будь то конфигурационные файлы, логи, бинарники или целые базы данных в CSV.

Go создавался для системного программирования, поэтому его авторы (Кен Томпсон, Роб Пайк и другие) встроили работу с файлами прямо в ядро языка. Никаких внешних зависимостей, только стандартная библиотека, которая работает быстро и предсказуемо. В этой статье мы разберем все сценарии: от простого чтения файла до рекурсивного обхода директорий и потоковой обработки больших данных.

Если вы пишете на Go и хотите уверенно работать с файловой системой — вы по адресу. Поехали!

Основы: открываем, читаем, закрываем

Самый первый шаг — это открытие файла. В Go за это отвечает функция os.Open(). Она возвращает указатель на файл и ошибку. Ошибки в Go — это не исключения, а значения. Их нужно проверять всегда.

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("example.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, string(buf[:n]))
}

Обратите внимание на defer file.Close(). Это идиома Go: отложенный вызов гарантирует, что файл закроется даже в случае паники или раннего возврата. Забыть закрыть файл — плохая практика, ведущая к утечке дескрипторов.

Чтение всего файла одной строкой

Если файл небольшой (до нескольких мегабайт), можно использовать os.ReadFile() (заменил устаревший ioutil.ReadFile). Это удобно для конфигов или текстовых данных.

data, err := os.ReadFile("config.yaml")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Функция сама открывает, читает и закрывает файл. Но помните: для больших файлов этот подход опасен — вы загрузите весь файл в память.

Запись в файл: создаем и дописываем

Для записи используйте os.Create() (создает новый файл или обрезает существующий) и os.OpenFile() для более тонкой настройки (например, добавление в конец).

// Создание файла
file, err := os.Create("output.txt")
if err != nil {
    panic(err)
}
defer file.Close()

// Запись строки
_, err = file.WriteString("Hello, Go файлы!\n")
if err != nil {
    panic(err)
}

// Запись байтов
bytes := []byte("Запись байтового среза\n")
_, err = file.Write(bytes)
if err != nil {
    panic(err)
}

Для добавления данных в конец существующего файла используйте флаги:

file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

file.WriteString("Новая запись в лог\n")

Флаг os.O_APPEND гарантирует, что данные допишутся в конец, а не перезапишут начало.

Работа с директориями: создаем, удаляем, обходим

Файловая система — это не только файлы, но и папки. Go предоставляет удобные функции для работы с директориями.

Создание и удаление папок

// Создать одну папку
err := os.Mkdir("newdir", 0755)
if err != nil {
    log.Fatal(err)
}

// Создать вложенные папки (как mkdir -p)
err = os.MkdirAll("path/to/nested/dir", 0755)
if err != nil {
    log.Fatal(err)
}

// Удалить пустую папку
err = os.Remove("newdir")
if err != nil {
    log.Fatal(err)
}

// Удалить папку со всем содержимым
err = os.RemoveAll("path")
if err != nil {
    log.Fatal(err)
}

Обход директории рекурсивно

Для обхода всех файлов и папок внутри директории используем filepath.Walk. Это одна из самых полезных функций в стандартной библиотеке.

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    root := "./mydata"
    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.IsDir() {
            fmt.Printf("Папка: %s\n", path)
        } else {
            fmt.Printf("Файл: %s (размер: %d байт)\n", path, info.Size())
        }
        return nil
    })
    if err != nil {
        fmt.Println("Ошибка обхода:", err)
    }
}

Функция обратного вызова получает путь, информацию о файле и ошибку. Если вернуть filepath.SkipDir, можно пропустить целую директорию.

Потоковая обработка: читаем большие файлы без перегрузки памяти

Когда файл весит гигабайты, загружать его целиком в память — самоубийство. В Go для этого есть bufio.Scanner и io.Reader.

Чтение больших файлов построчно

file, err := os.Open("huge_log.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
// Увеличиваем буфер для длинных строк (по умолчанию 64KB)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

lineCount := 0
for scanner.Scan() {
    line := scanner.Text()
    // Обрабатываем строку
    lineCount++
    if lineCount%100000 == 0 {
        fmt.Printf("Обработано %d строк\n", lineCount)
    }
}

if err := scanner.Err(); err != nil {
    log.Fatal("Ошибка сканирования:", err)
}
fmt.Printf("Всего строк: %d\n", lineCount)

Scanner читает файл порциями, не загружая все в память. Идеально для логов, CSV и других текстовых форматов.

Копирование файлов через io.Copy

Для бинарных файлов (изображения, видео) используйте io.Copy — он эффективно переносит данные между потоками.

src, _ := os.Open("source.jpg")
defer src.Close()
dst, _ := os.Create("dest.jpg")
defer dst.Close()

written, err := io.Copy(dst, src)
if err != nil {
    log.Fatal                

Блоги

Рекомендаций по книгам