Работа с файлами
Работа с файлами - одна из базовых задач бэкенда: прочитать конфиг, записать лог, обработать 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())
}
Типичные ошибки
- Забыть
defer file.Close()- утечка дескрипторов. При 1000+ открытий система откажет. - Забыть
writer.Flush()- потеря данных в конце файла. - Читать огромный файл через
os.ReadFile- OOM (out of memory). Используйтеbufio.Scannerилиio.Copy. - Не проверять
scanner.Err()- ошибки чтения проглатываются молча. - Проверять файл перед открытием - race condition. Открывайте сразу и обрабатывайте ошибку.
Что запомнить
os.ReadFile/os.WriteFile- для маленьких файлов (конфиги, JSON)bufio.Scanner- для построчного чтения (логи, CSV)io.Copy- для больших бинарных файлов (потоково, без загрузки в память)bufio.Writer- для эффективной построчной записиdefer file.Close()- всегдаos.CreateTemp- для временных файлов