Основы Go: ввод-вывод файловой системы



Книга Основы Go: ввод-вывод файловой системы

Введение


Чтение с диска и запись на диск, а также перемещение по файловой системе  —  это основной элемент в любом языке. Узнаем, как все это делать в 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 := "
Adding 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.


Спасибо за внимание!


1414   0  

Comments

    Ничего не найдено.