Содержание

Руководство по работе с файлами в Go

Файлы - это один из первых способов, через который программа начинает работать с реальным миром. До файлов у нас есть только переменные в памяти: программа запустилась, что-то посчитала и завершилась. После завершения память очищается. Файл же остается на диске.

В проектах Praxis файлы встречаются постоянно: список банковских префиксов, логи, архивы, директории для синхронизации, конфиги, временные файлы, результаты отчетов. Поэтому важно не просто знать os.ReadFile, а понимать весь базовый набор: открыть, прочитать, записать, закрыть, пройти по директории, скопировать, удалить и правильно обработать ошибку.

Ментальная модель: файл как поток байтов

Файл можно представить как длинную ленту байтов:

H e l l o \n W o r l d 0 1 2 3 4 5 6 7 8 9 10

Когда программа читает файл, у нее есть текущая позиция. Сначала позиция в начале. После чтения нескольких байтов позиция сдвигается. Это похоже на курсор в текстовом редакторе.

Некоторые операции читают файл целиком в память. Это удобно, если файл маленький. Другие читают потоками или строками. Это нужно для больших файлов.

Когда читать файл целиком

Для маленьких файлов используйте os.ReadFile:

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

os.ReadFile возвращает []byte, потому что файл хранит байты, а не обязательно текст. Если это текст, можно преобразовать в string.

Подходит для:

  • конфигов;
  • маленьких JSON/YAML файлов;
  • тестовых fixtures;
  • небольших входных данных.

Не подходит для больших логов, архивов и файлов на сотни мегабайт.

Открытие файла

Если нужно читать постепенно, сначала откройте файл:

file, err := os.Open("data.txt") if err != nil { return err } defer file.Close()

os.Open открывает файл только для чтения. defer file.Close() гарантирует, что файл будет закрыт при выходе из функции.

Почему закрытие важно:

  • у процесса ограничено количество открытых файлов;
  • данные при записи могут буферизоваться;
  • на Windows открытый файл может мешать удалению или переименованию;
  • незакрытые ресурсы усложняют диагностику.

Чтение построчно через bufio.Scanner

bufio.Scanner удобен для текстовых файлов:

file, err := os.Open("app.log") if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() fmt.Println(line) } if err := scanner.Err(); err != nil { return err }

Что происходит:

  1. scanner.Scan() пытается прочитать следующий токен.
  2. По умолчанию токен - строка.
  3. scanner.Text() возвращает текущую строку.
  4. После цикла нужно проверить scanner.Err().

Чтение через bufio.Reader

bufio.Reader дает больше контроля:

reader := bufio.NewReader(file) for { line, err := reader.ReadString('\n') if err != nil && !errors.Is(err, io.EOF) { return err } if line != "" { fmt.Print(line) } if errors.Is(err, io.EOF) { break } }

io.EOF означает "конец файла". Это не авария, а нормальный сигнал завершения чтения.

Запись всего файла

Для простой записи:

data := []byte("hello\n") if err := os.WriteFile("result.txt", data, 0644); err != nil { return err }

Права 0644 означают:

  • владелец может читать и писать;
  • группа может читать;
  • остальные могут читать.

Создание файла и потоковая запись

Если данные формируются постепенно:

file, err := os.Create("report.txt") if err != nil { return err } defer file.Close() writer := bufio.NewWriter(file) for _, row := range rows { fmt.Fprintf(writer, "%s,%d\n", row.Name, row.Count) } if err := writer.Flush(); err != nil { return err }

bufio.Writer накапливает данные в памяти и пишет крупными кусками. Это быстрее, чем делать много маленьких записей на диск.

os.OpenFile: режимы открытия

os.OpenFile нужен, когда Open, Create или WriteFile слишком простые:

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

Частые флаги:

ФлагЗначение
os.O_RDONLYтолько чтение
os.O_WRONLYтолько запись
os.O_RDWRчтение и запись
os.O_CREATEсоздать, если файла нет
os.O_APPENDписать в конец
os.O_TRUNCочистить файл при открытии

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

info, err := os.Stat("data.txt") if err != nil { if errors.Is(err, os.ErrNotExist) { fmt.Println("файла нет") return nil } return err } fmt.Println(info.Size())

os.Stat возвращает FileInfo: размер, время изменения, режим доступа и признак директории.

Пути: filepath.Join

Не собирайте пути через "/" вручную:

path := "data/" + filename

Используйте filepath.Join:

path := filepath.Join("data", "archive", filename)

Так код корректнее работает на разных ОС и аккуратнее обрабатывает лишние разделители.

Директории

Создать одну директорию:

err := os.Mkdir("logs", 0755)

Создать всю цепочку:

err := os.MkdirAll("data/archive/2026", 0755)

Прочитать содержимое директории:

entries, err := os.ReadDir("data") if err != nil { return err } for _, entry := range entries { if entry.IsDir() { fmt.Println("dir:", entry.Name()) } else { fmt.Println("file:", entry.Name()) } }

Рекурсивный обход через filepath.WalkDir

filepath.WalkDir проходит по директории и всем вложенным файлам:

err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } if entry.IsDir() { return nil } if strings.HasSuffix(entry.Name(), ".log") { fmt.Println(path) } return nil })

Это основной инструмент для задач вроде "найди все .txt файлы", "посчитай размеры", "синхронизируй директории".

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

Не нужно читать файл целиком, чтобы скопировать его:

func copyFile(srcPath, dstPath string) error { src, err := os.Open(srcPath) if err != nil { return fmt.Errorf("open source: %w", err) } defer src.Close() dst, err := os.Create(dstPath) if err != nil { return fmt.Errorf("create destination: %w", err) } defer dst.Close() if _, err := io.Copy(dst, src); err != nil { return fmt.Errorf("copy data: %w", err) } return nil }

io.Copy читает из src и пишет в dst потоками. Это подходит для больших файлов.

Seek и Truncate

Seek перемещает позицию чтения или записи:

_, err := file.Seek(0, io.SeekStart)

Так можно вернуться в начало файла после чтения первых байтов.

Truncate меняет размер файла:

err := file.Truncate(0)

Это очищает файл. Такие операции нужны в архиваторах, бинарных форматах, обработке multipart-файлов и низкоуровневых задачах.

Удаление и переименование

Удалить файл:

err := os.Remove("old.txt")

Переименовать или переместить:

err := os.Rename("draft.txt", "final.txt")

Удалить директорию со всем содержимым:

err := os.RemoveAll("tmp")

RemoveAll опасен, если путь собран неправильно. Перед удалением полезно логировать путь или проверять, что он внутри ожидаемой рабочей директории.

Временные файлы в тестах

В тестах не создавайте файлы прямо в проекте. Используйте t.TempDir():

func TestReadConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.txt") if err := os.WriteFile(path, []byte("debug=true"), 0644); err != nil { t.Fatal(err) } config, err := ReadConfig(path) if err != nil { t.Fatal(err) } if !config.Debug { t.Fatal("expected debug mode") } }

Go сам удалит временную папку после теста.

Частые ошибки

  1. Не закрыли файл через defer file.Close().
  2. Прочитали большой файл через os.ReadFile и получили лишний расход памяти.
  3. Не проверили scanner.Err() после цикла.
  4. Не вызвали writer.Flush().
  5. Собрали путь через строки вместо filepath.Join.
  6. Сравнили ошибку строкой вместо errors.Is.
  7. Использовали RemoveAll с непроверенным путем.

Как выбрать функцию

ЗадачаИнструмент
Прочитать маленький файл целикомos.ReadFile
Записать маленький файл целикомos.WriteFile
Читать текст построчноbufio.Scanner
Читать строки с большим контролемbufio.Reader
Писать много строкbufio.Writer
Проверить файлos.Stat
Пройти по директорииfilepath.WalkDir
Скопировать поток данныхio.Copy
Создать директорииos.MkdirAll
Собрать путьfilepath.Join

Полный пример: собрать список .log файлов

func FindLogFiles(root string) ([]string, error) { var files []string err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } if entry.IsDir() { return nil } if strings.HasSuffix(entry.Name(), ".log") { files = append(files, path) } return nil }) if err != nil { return nil, err } return files, nil }

Этот пример соединяет несколько базовых идей: рекурсивный обход, проверку типа entry, фильтрацию по имени и возврат ошибки наверх.

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи