Руководство по работе с файлами в 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 }
Что происходит:
scanner.Scan()пытается прочитать следующий токен.- По умолчанию токен - строка.
scanner.Text()возвращает текущую строку.- После цикла нужно проверить
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 сам удалит временную папку после теста.
Частые ошибки
- Не закрыли файл через
defer file.Close(). - Прочитали большой файл через
os.ReadFileи получили лишний расход памяти. - Не проверили
scanner.Err()после цикла. - Не вызвали
writer.Flush(). - Собрали путь через строки вместо
filepath.Join. - Сравнили ошибку строкой вместо
errors.Is. - Использовали
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, фильтрацию по имени и возврат ошибки наверх.