Введение
Чтение с диска и запись на диск, а также перемещение по файловой системе — это основной элемент в любом языке. Узнаем, как все это делать в Go с помощью пакета os, который позволяет взаимодействовать с функциональностью операционной системы.
Что обсудим:
Файлы
Создание и открытие файлов
Чтение файлов
Запись и добавление в файлы
Удаление файлов
Каталоги
Создание каталогов
Чтение каталогов и перемещение по ним
Пройдемся по каталогу
Файлы
Создание и открытие файлов
Создание файлов происходит с помощью os.Create
, а открытие — с помощью os.Open
. И там и там принимается путь к файлу и возвращается структура File
, а в случае неуспеха — ошибка с nil
.
import (
"os"
)
func createFile(){
filePtr, err := os.Create("./test/creating.txt");
if err != nil {
log.Fatal(err);
}
defer filePtr.Close(); // закрываем файл
// Читаем с файла и записываем в файл
}
func openFile(){
filePtr, err := os.Open("./test/creating.txt");
if err != nil {
log.Fatal(err);
}
defer filePtr.Close(); // закрываем файл
// Читаем с файла и записываем в файл
}
Когда os.Create
вызывается в существующем файле, он этот файл обрезает: данные файла стираются. В то же время вызов os.Open
в несуществующем файле приводит к ошибке.
В случае успеха возвращенная структура File
используется для записи и чтения данных в файле (дальше в статье будет приведен пример того, как файл открывается, читается фрагмент за фрагментом и закрывается).
После взаимодействия с возвращенным файлом закрываем его с помощью File.Close
.
Чтение файлов
Один из способов обработки файла — прочитать сразу все содержащиеся в нем данные. Делается это с использованием os.ReadFile
. Вводимые данные — это путь к файлу, а выходные данные — это байтовый массив данных файла и ошибка в случае неуспеха.
import (
"log"
"os"
)
/*
Содержание test.txt:
Hello World!
*/
func readFileContents(){
bytes, err := os.ReadFile("test.txt");
if err != nil {
log.Fatal(err);
}
fileText = string(bytes[:]); // fileText — это «Hello World!»
}
При обработке текстового файла для получения текста файла нужно преобразовать получаемый на выходе массив байтов в строку.
Имейте в виду, что os.ReadFile
прочитает весь файл и загрузит его данные в память. И чем больше файл, тем больший объем памяти будет потребляться при использовании os.ReadFile
.
Эффективный с точки зрения потребления памяти подход связан с пофрагментной обработкой файла, осуществляемой с помощью os.Open
.
func readFileChunkWise() {
chunkSize := 10 // обработка сразу 10 байтов файла
b := make([]byte, chunkSize)
file, err := os.Open("./folder/test.txt);
if err != nil {
log.Fatal(err)
}
for {
bytesRead, _ := file.Read(b);
if bytesRead == 0 { // bytesRead будет равен 0 в конце файла.
break
}
// обрабатываются прочитанные на данный момент байты
process(b, bytesRead);
}
file.Close();
}
После открытия файла происходит многократный вызов File.Read
до EOF (конца файла).
File.Read
принимает байтовый массив b
и загружает до len(b)
байтов из файла в b
. А затем возвращает количество прочитанных байтов bytesRead
и ошибку, если что-то пойдет не так. При bytesRead
равным 0 нажимаем EOF и заканчиваем обработку файла.
В приведенном выше коде из файла загружается максимум 10 байтов. Они обрабатываются, и этот процесс повторяется до EOF (конца файла).
В случае с более крупными файлами при этом подходе потребляется намного меньше памяти, чем при загрузке сразу всего файла.
Запись и добавление в файлы
Для записи байтов в файл существует аналогичная os.ReadFile
функция os.WriteFile
.
import (
"log"
"os"
)
func writeFileContents() {
content := "Something to write";
/* os.WriteFile принимает путь к файлу, []byte содержимого файла
и биты полномочий, в случае если файл не существует */
err := os.WriteFile("./test.txt", []byte(content), 0666);
if err != nil {
log.Fatal(err);
}
}
Что следует учесть при использовании os.WriteFile
:
- Обязательно преобразуйте данные для записи в
[]byte
, прежде чем передавать их вos.WriteFile
. - Биты полномочий необходимы для создания файла, если он еще не существует. Но на них заострять внимание не стоит.
- Если путь к файлу уже существует,
os.WriteFile
переопределит исходные данные в файле с помощью новых записываемых данных.
os.WriteFile
хорош для создания нового файла или его переопределения. Но он не работает, когда нужно сделать добавление к имеющемуся содержимому файла. Для добавления в файл нужно задействовать os.OpenFile
.
Согласно документации, os.OpenFile
— это более обобщенная версия os.Open
и os.Create
. И os.Create
, и os.Open
внутренне вызывают его.
Кроме пути к файлу, os.OpenFile
принимает флаги int
и perm
(биты полномочий) и возвращает структуру File
. Для выполнения таких операций, как чтение и запись, в os.OpenFile
должна быть указана правильная комбинация флагов
.
const (
// Должно быть указано либо O_RDONLY, либо O_WRONLY, либо O_RDWR.
O_RDONLY int = syscall.O_RDONLY // файл открывается только для чтения.
O_WRONLY int = syscall.O_WRONLY // файл открывается только для записи.
O_RDWR int = syscall.O_RDWR // файл открывается для чтения и записи.
// Остальные значения пропускаются через логическое ИЛИ, чтобы контролировать поведение.
O_APPEND int = syscall.O_APPEND // добавление данных в файл при записи.
O_CREATE int = syscall.O_CREAT // создание нового файла, если не существует.
O_EXCL int = syscall.O_EXCL // используется с O_CREATE, файл не должен существовать.
O_SYNC int = syscall.O_SYNC // открытие для синхронного ввода-вывода.
O_TRUNC int = syscall.O_TRUNC // обрезается обычный файл с возможностью записи при открытии.
)
Источник: https://golang.org/pkg/os/#pkg-constants
O_APPEND
и O_WRONLY
объединяют с побитовым ИЛИ и передают в os.OpenFile
для получения структуры File
. После этого при вызове File.Write
с любыми передаваемыми данными эти данные будут добавлены в конец файла.
import (
"log"
"os"
)
/*
append.txt изначально:
Имеющаяся строка
append.txt после вызова appendToFile:
Имеющаяся строка
Добавление новой строки
*/
func appendToFile(){
content := "\nAdding a new line";
file, err := os.OpenFile("append.txt", os.O_APPEND | os.O_WRONLY, 0644);
defer file.Close();
if err != nil {
log.Fatal(err);
}
file.Write([]byte(content));
}
Удаление файлов
os.Remove
принимает путь к файлу или пустому каталогу и удаляет этот файл/каталог. Если файл не существует, будет возвращена ошибка с nil
.
import (
"log"
"os
)
func removeFile(){
err := os.Remove("./removeFolder/remove.txt");
if err != nil{
log.Fatal(err);
}
}
Освоив основы работы с файлами, перейдем теперь к каталогам.
Каталоги
Создание каталогов
Для создания нового каталога используется os.Mkdir
. Эта функция принимает имя каталога и биты полномочий, и так создается новый каталог. Если os.Mkdir
не создаст каталог, будет возвращена ошибка с nil
.
import (
"log"
"os"
)
func makeDir(){
err := os.Mkdir("newDirectory", 0755);
if err != nil {
log.Fatal(err);
}
}
В некоторых ситуациях бывают нужны временные каталоги, которые существуют только во время выполнения программы. Для создания таких каталогов используется os.MkdirTemp
.
import (
"log"
"os"
)
/*
os.MkdirTemp принимает путь для создания временного каталога и шаблон.
os.MkdirTemp создаст новый каталог с именем, состоящим из шаблона и случайной строки.
Пример. Для шаблона «transform» временный каталог будет такой:
./temporary/transform952209073
*/
func makeTempDir(){
dirName, err := os.MkdirTemp("./temporary", "transform");
defer os.RemoveAll(dirName); // удаляем все содержимое каталога
if err != nil {
log.Fatal(err);
}
}
os.MkdirTemp
снабжает создаваемые временные каталоги уникальными именами, даже когда происходят вызовы от нескольких горутин или программ (источник). Закончив работу с временным каталогом, обязательно удалите вместе с его содержимым с помощью os.RemoveAll
.
Чтение каталогов и перемещение по ним
Сначала с помощью os.Getwd
получим текущий рабочий каталог:
import (
"log"
"os"
)
func getWd() {
dir, err = os.Getwd()
if err != nil {
log.Fatal(err);
}
return dir;
}
Затем для изменения рабочего каталога задействуем os.Chdir
:
import (
"log"
"os"
)
func navigate(){
os.Getwd() // Рабочий каталог: ./folder
os.Chdir("./item"); // Рабочий каталог: ./folder/item
os.Chdir("../"); // Рабочий каталог: ./folder
}
В добавок к изменению рабочего каталога у нас есть возможность получить дочерний каталог. Делается это с помощью os.ReadDir
. Эта функция принимает путь к каталогу и возвращает массив структур DirEntry
и ошибку c nil
в случае неуспеха.
type DirEntry interface {
// Name возвращает имя файла (или подкаталога), описываемое записью entry.
// Это имя не весь путь, а только конечный его элемент (базовое имя).
// Например, Name вернет «hello.go», а не «/home/gopher/hello.go».
Name() string
// IsDir сообщает, описывает ли запись каталог.
IsDir() bool
// Type возвращает разряды типа для записи.
// Разряды типа — это подмножество обычных разрядов FileMode, возвращаемых FileMode.Type method.
Type() FileMode
// Info возвращает FileInfo для файла или подкаталога, описываемого записью.
// Возвращаемый FileInfo бывает с момента чтения исходного каталога
// или с момента вызова к Info. Когда файл удален или переименован
// с момента чтения каталога, Info возвращает ошибку, удовлетворяющую errors.Is(err, ErrNotExist).
// Когда запись обозначает символическую ссылку, Info сообщает информацию о самой ссылке,
// а не о цели ссылки.
Info() (FileInfo, error)
}
Источник: https://golang.org/pkg/io/fs/#DirEntry
Вот пример использования:
import (
"fmt"
"log"
"os"
)
/*
Допустим, это была структура каталогов теста:
- test
- a.txt
- b
- c.txt
getDirectoryContents will print out "a.txt" and "b".
*/
func getDirectoryContents(){
entries, err := os.ReadDir("./test");
if err != nil {
log.Fatal(err);
}
//выполняется итеративный обход объектов каталога и выводится их название.
for _, entry := range(entries) {
fmt.Println(entry.Name());
}
}
Пройдемся по каталогу
Используя os.Chdir
и os.ReadDir
, мы проходимся по всем файлам и подкаталогам родительского каталога. Но в пакете path/filepath есть функция filepath.WalkDir
, позволяющая сделать это более элегантно.
filepath.WalkDir
принимает каталог root
, из которого мы стартуем, и функцию обратного вызова fn
следующего типа:
type WalkDirFunc func(path string, d DirEntry, err error) error
fn
будет вызываться в каждом файле и подкаталоге каталога root
. Вот пример подсчета всех файлов в корневом каталоге:
import (
"fmt"
"io/fs"
"path/filepath"
)
// пример подсчета всех файлов в корневом каталоге
func countFiles() int {
count := 0;
filepath.WalkDir(".", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if !file.IsDir() {
fmt.Println(path);
count++;
}
return nil;
});
return count;
}
В path/filepath
есть еще одна функция filepath.Walk
с поведением, аналогичным filepath.WalkDir
. Однако в документации сказано, что filepath.Walk
менее эффективна, чем filepath.WalkDir
. Поэтому лучше использовать filepath.WalkDir
.
Заключение
Надеюсь, эта статья помогла вам сделать еще один шаг в изучении Go в том, что касается основ работы с файлами и каталогами. Закрепить материал рекомендую реализацией вашей собственной версии filepath.WalkDir
.
Спасибо за внимание!
Читайте также:
- Реализация интерфейсов в Golang
- Бенчмарки в Golang: тестируем производительность кода
- Оптимизация структур в Golang для эффективного распределения памяти
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Ramki Pitchala: Go Basics: Filesystem IO