Почему работа с файлами в 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