Обработка файлов в Go

Обработка файлов — одна из самых распространённых операций в Go: чтение конфигурации, запись логов и сохранение данных зависят от неё. Стандартная библиотека Go предоставляет лаконичный, но мощный набор интерфейсов файлового ввода-вывода, охватывающий сценарии от однократного чтения/записи до потоковой обработки.

Существует 5 основных пакетов, связанных с обработкой файлов, каждый из которых выполняет свою роль:

Go File Handling Core Packages os Core: create, read/write, delete, permissions io Generic Reader/Writer Interface bufio Buffered read/write, performance optimization filepath Cross-platform path handling ioutil ⚠ Deprecated in Go 1.16 → os / io Typical usage flow os.Open() bufio.Scanner Process data defer Close()

Примечание о версии:ioutilПакет был признан устаревшим в Go 1.16, а его функциональность была перемещена вos(напримерos.ReadFile、os.WriteFile) иio(напримерio.ReadAll). Новый код должен напрямую использоватьosиioпакет.


1. Создание файла

os.CreateСоздать новый файл. Если файл уже существует,он будет усечён (содержимое очищено). Возвращённый файловый объект необходимо закрыть, чтобы освободить системные ресурсы:

Пример

package main

import (
        "log"
        "os"
)

func main() {
        // os.Create создаёт файл (усекает, если он уже существует)
        file, err := os.Create("test.txt")
        if err != nil {
                log.Fatal(err)
        }
        defer file.Close() // defer гарантирует, что файл будет закрыт при завершении функции

        // Записать содержимое, чтобы убедиться, что создание прошло успешно
        file.WriteString("File created successfully\n")
        log.Println("File created successfully")
}

defer file.Close()является лучшей практикой для файловых операций в Go.deferгарантирует, что даже если последующий код вызовет панику, файл будет корректно закрыт, что позволит избежать утечек файловых дескрипторов.


2. Открытие и закрытие файлов

osПакет предоставляет три способа открытия файлов, подходящих для разных сценариев:

Функция Mode Описание
os.Open(name) Только для чтения Самый простой способ — открыть только для чтения
os.Create(name) Чтение/Запись + Создание/Усечение Создать новый файл или очистить существующий файл
os.OpenFile(name, flag, perm) Индивидуальный Можно указать флаги, такие как чтение/запись, добавление, создание и т. д.

Пример

package main

import (
        "fmt"
        "os"
)

func main() {
        // Способ 1: открыть только для чтения
        file, err := os.Open("example.txt")
        if err != nil {
                fmt.Println("Ошибка открытия:, err)
                return
        }
        defer file.Close()
        fmt.Println("Файл успешно открыт")

        // Способ 2: OpenFile пользовательский режим
        // os.O_WRONLY только для записи
        // os.O_CREATE создать, если файл не существует
        // os.O_APPEND режим добавления
        // os.O_TRUNC усечь, если файл существует
        f, err := os.OpenFile("log.txt",
                os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
        if err != nil {
                fmt.Println("Ошибка открытия:, err)
                return
        }
        defer f.Close()
        f.WriteString("Append a line of log\n")
}

Описания флагов OpenFile

Флаги Описание
os.O_RDONLY Только для чтения (по умолчанию)
os.O_WRONLY Только для записи
os.O_RDWR Чтение-запись
os.O_APPEND Режим добавления, записывает содержимое в конец файла
os.O_CREATE Создать, если файл не существует
os.O_TRUNC Очистить содержимое файла при открытии
os.O_EXCL Используется с CREATE, выдаёт ошибку, если файл уже существует

3. Чтение файлов

Go предоставляет три основных способа чтения файлов; выбирайте в зависимости от размера файла и требований к обработке:

Three ways to read files Read all at once os.ReadFile("file.txt") Suitable for small files (config, JSON, etc.) Automatically opens/closes, simplest Line-by-line reading bufio.NewScanner(file) Suitable for large files (logs, CSV, etc.) Memory-friendly, processes line by line Streaming read io.ReadAll(reader) Suitable for network responses and already-open files Generic Reader interface

3.1 Однократное чтение (рекомендуется для небольших файлов)

os.ReadFileявляется самым простым способом — автоматически открыть, прочитать и закрыть файл за один шаг:

Пример

package main

import (
        "fmt"
        "log"
        "os"
)

func main() {
        // Прочитать весь файл за один раз (Go 1.16+)
        data, err := os.ReadFile("config.json")
        if err != nil {
                log.Fatal(err)
        }

        fmt.Println(string(data))
}

3.2 Построчное чтение (рекомендуется для больших файлов)

Для больших файлов используйтеbufio.ScannerОбрабатывайте построчно, чтобы не загружать всё в память сразу:

Пример

package main

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

func main() {
        file, err := os.Open("large-file.log")
        if err != nil {
                log.Fatal(err)
        }
        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 {
                log.Fatal("Ошибка чтения:", err)
        }
}

Примечание:bufio.ScannerМаксимальная длина строки по умолчанию — 64KB. Если файл содержит слишком длинные строки, вам нужно, при вызовеScan()Пресетscanner.Buffer(make([]byte, 0), maxSize)чтобы увеличить буфер.

3.3 Чтение с помощью io.ReadAll

Когда у вас уже есть открытыйio.Reader(например, сетевые ответы, открытые файлы), вы можете использоватьio.ReadAll:

Пример

package main

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

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

        // Прочитать все данные из Reader
        data, err := io.ReadAll(file)
        if err != nil {
                log.Fatal(err)
        }

        fmt.Println(string(data))
}

4. Запись в файл

Go предоставляет несколько способов записи: от простой однократной записи до высокопроизводительной буферизованной записи:

4.1 Однократная запись

os.WriteFileЗаписать данные в файл за один раз (перезаписав существующее содержимое):

Пример

package main

import (
        "log"
        "os"
)

func main() {
        content := []byte("Hello, Go!\nЭто вторая строка\n")

        // 0644: владелец: чтение/запись, остальные: только чтение
        err := os.WriteFile("output.txt", content, 0644)
        if err != nil {
                log.Fatal(err)
        }
        log.Println("Write successful")
}

4.2 Запись с использованием объекта File

Запись через объект file позволяет записывать содержимое в несколько проходов:

Пример

package main

import (
        "fmt"
        "log"
        "os"
)

func main() {
        file, err := os.Create("output.txt")
        if err != nil {
                log.Fatal(err)
        }
        defer file.Close()

        // Метод 1: запись строки
        file.WriteString("Write string directly\n")

        // Метод 2: запись среза байтов
        data := []byte("Write byte slice\n")
        file.Write(data)

        // Метод 3: форматированная запись
        fmt.Fprintf(file, "Форматированная запись: %d + %d = %d\n", 3, 4, 7)
}

4.3 Буферизованная запись (рекомендуется для больших объёмов данных)

bufio.WriterСначала он записывает данные в буфер в памяти, затем записывает на диск пакетами, как только накопится достаточно, что значительно сокращает количество операций ввода-вывода.Обязательно вызовите перед завершениемFlush():

Пример

package main

import (
        "bufio"
        "log"
        "os"
)

func main() {
        file, err := os.Create("buffered-output.txt")
        if err != nil {
                log.Fatal(err)
        }
        defer file.Close()

        // Создать буферизованный writer
        writer := bufio.NewWriter(file)

        // Записать несколько строк (сначала в буфер, а не сразу на диск)
        for i := 0; i < 1000; i++ {
                writer.WriteString("Это строка " + itoa(i) + " строка\n")
        }

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

// Простая вспомогательная функция для преобразования int в string
func itoa(n int) string {
        return fmt.Sprintf("%d", n)
}

5. Добавление в файлы

Использованиеos.O_APPENDЭтот флаг добавляет содержимое в конец файла, не перезаписывая существующие данные:

Пример

package main

import (
        "log"
        "os"
        "time"
)

func main() {
        // O_APPEND: режим добавления
        // O_CREATE: создать, если он не существует
        // O_WRONLY: только для записи
        file, err := os.OpenFile("app.log",
                os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
                log.Fatal(err)
        }
        defer file.Close()

        // Дозаписывает при каждом запуске, не перезаписывает
        timestamp := time.Now().Format("2006-01-02 15:04:05")
        file.WriteString("[" + timestamp + "] Приложение запущено"\n")
}

Сравнение:os.CreateОн очистит существующее содержимое;os.OpenFileКоординацияO_APPENDОн добавит содержимое в конец. В сценариях логирования обязательно используйте режим добавления.


6. Удаление и переименование файлов

Пример

package main

import (
        "log"
        "os"
)

func main() {
        // Переименовать / переместить файл
        err := os.Rename("old.txt", "new.txt")
        if err != nil {
                log.Fatal("Ошибка переименования:", err)
        }
        log.Println("Переименование выполнено успешно")

        // Удалить файл
        err = os.Remove("temp.txt")
        if err != nil {
                log.Fatal("Ошибка удаления:", err)
        }
        log.Println("Удаление выполнено успешно")
}

7. Информация о файле и проверка

7.1 Получение информации о файле

Пример

package main

import (
        "fmt"
        "log"
        "os"
)

func main() {
        info, err := os.Stat("test.txt")
        if err != nil {
                log.Fatal(err)
        }

        fmt.Println("Имя файла: ", info.Name())
        fmt.Println("Размер: ", info.Size(), "bytes")
        fmt.Println("Права доступа: ", info.Mode())
        fmt.Println("Изменён: ", info.ModTime())
        fmt.Println("Это каталог: ", info.IsDir())
}

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

В Go нет отдельной функции "file exists"; с помощьюos.Stat + os.IsNotExistОпределите:

Пример

package main

import (
        "fmt"
        "os"
)

// fileExists проверяет, существует ли файл
func fileExists(path string) bool {
        _, err := os.Stat(path)
        return !os.IsNotExist(err)
}

func main() {
        if fileExists("test.txt") {
                fmt.Println("Файл существует")
        } else {
                fmt.Println("Файл не существует")
        }
}

8. Операции с каталогами

Пример

package main

import (
        "fmt"
        "log"
        "os"
)

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

        // Рекурсивно создать многоуровневые каталоги (автоматически создаёт родительские каталоги, даже если они не существуют)
        err = os.MkdirAll("path/to/deep/dir", 0755)
        if err != nil {
                log.Fatal(err)
        }

        // Чтение содержимого каталога
        entries, err := os.ReadDir(".")
        if err != nil {
                log.Fatal(err)
        }
        for _, entry := range entries {
                mark := "&#x1f4c4;"
                if entry.IsDir() {
                        mark = "&#x1f4c1;"
                }
                fmt.Printf("%s %s\n", mark, entry.Name())
        }

        // Удалить пустой каталог
        os.Remove("newdir")

        // Рекурсивно удалить каталог и все его содержимое (&#x26a0; используйте с осторожностью)
        os.RemoveAll("path")
}

9. Расширенные операции

9.1 Копирование файлов

Использованиеio.CopyЭффективно копировать данные между двумя файлами:

Пример

package main

import (
        "io"
        "log"
        "os"
)

// copyFile — это обобщённая функция для копирования файлов
func copyFile(src, dst string) (int64, error) {
        srcFile, err := os.Open(src)
        if err != nil {
                return 0, err
        }
        defer srcFile.Close()

        dstFile, err := os.Create(dst)
        if err != nil {
                return 0, err
        }
        defer dstFile.Close()

        // io.Copy автоматически управляет буфером для эффективного копирования
        return io.Copy(dstFile, srcFile)
}

func main() {
        n, err := copyFile("source.txt", "destination.txt")
        if err != nil {
                log.Fatal(err)
        }
        log.Printf("Копирование завершено, всего %d байт", n)
}

9.2 Временные файлы и каталоги

Пример

package main

import (
        "fmt"
        "log"
        "os"
)

func main() {
        // Создать временный файл (автоматически именуется, префикс — "app-")
        tmpFile, err := os.CreateTemp("", "app-*.txt")
        if err != nil {
                log.Fatal(err)
        }
        defer os.Remove(tmpFile.Name()) // Удалить после использования
        fmt.Println("Временный файл:", tmpFile.Name())

        // Создать временный каталог
        tmpDir, err := os.MkdirTemp("", "app-*")
        if err != nil {
                log.Fatal(err)
        }
        defer os.RemoveAll(tmpDir) // Удалить после использования
        fmt.Println("Временный каталог:", tmpDir)
}

9.3 Рекурсивный обход каталогов

filepath.WalkВы можете рекурсивно обойти все файлы и подкаталоги в дереве каталогов:

Пример

package main

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

func main() {
        root := "." // Начать обход из текущего каталога

        err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
                if err != nil {
                        return err // При возникновении ошибки можно выбрать: пропустить или вернуть
                }

                // Вывести путь и размер
                size := ""
                if !info.IsDir() {
                        size = fmt.Sprintf(" (%d bytes)", info.Size())
                }
                fmt.Println(path + size)
                return nil
        })

        if err != nil {
                log.Fatal(err)
        }
}

9.4 Кроссплатформенное объединение путей

Никогда не используйте конкатенацию строк для построения путей(например,dir + "/" + file), используйтеfilepath.JoinДля обеспечения кроссплатформенной совместимости:

// 错误:Windows 上路径分隔符是 \
path := "dir" + "/" + "file.txt"

// 正确:自动适配当前操作系统
path := filepath.Join("dir", "file.txt")

Быстрый справочник по сценариям

Сценарии Рекомендуемый подход Ключевой код
Чтение небольшого файла os.ReadFile data, err := os.ReadFile("file.txt")
Чтение больших файлов построчно bufio.Scanner scanner := bufio.NewScanner(file)
Запись небольшого файла os.WriteFile os.WriteFile("f.txt", data, 0644)
Запись больших объёмов данных bufio.Writer writer := bufio.NewWriter(file)
Дозапись в журнал os.OpenFile + O_APPEND os.OpenFile("log", os.O_APPEND|os.O_WRONLY, 0644)
Копирование файла io.Copy io.Copy(dst, src)
Обход каталога filepath.Walk filepath.Walk(".", callback)
Объединение путей filepath.Join filepath.Join("dir", "file.txt")
Проверка существования файла os.Stat + os.IsNotExist _, err := os.Stat(path)
Создание временных файлов os.CreateTemp os.CreateTemp("", "prefix-*.txt")

Описание прав доступа к файлу:0644Указывает, что владелец может читать и записывать (6=4+2), а группа и остальные пользователи имеют доступ только для чтения (4). Для каталогов обычно используется0755(добавляет разрешение на выполнение, иначе вы не сможете войти в каталог)

Другие расширения