Файлы
Стандартные библиотеки Go для обработки файлов включают:
os— реализация взаимодействия с файловой системой ОСio— абстрактный уровень для чтения и записи IOfs— абстрактный уровень файловой системы
В этой статье мы рассмотрим основы обработки файлов в Go.
Открытие
Два распространённых способа открытия файла — использование функций Open и OpenFile из пакета os. Функция Open возвращает указатель на файл и ошибку:
func Open(name string) (*File, error)Функция OpenFile предоставляет более детальное управление, а функция Open является простой обёрткой вокруг OpenFile:
func OpenFile(name string, flag int, perm FileMode) (*File, error)Рассмотрим первый способ — просто указываем имя файла:
func main() {
file, err := os.Open("README.txt")
fmt.Println(file, err)
}Путь поиска файла по умолчанию — это путь к файлу go.mod проекта. Поскольку в проекте нет файла README.txt, будет возвращена ошибка:
<nil> open README.txt: The system cannot find the file specified.Поскольку типов ошибок IO много, необходимо вручную проверять существование файла. Пакет os предоставляет удобные функции для этого:
func main() {
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
fmt.Println("Файл не существует")
} else if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file)
}
}Повторный запуск выведет:
Файл не существуетПервый способ чтения файла поддерживает только режим только для чтения, файл нельзя модифицировать:
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}Функция OpenFile позволяет контролировать больше деталей, таких как дескрипторы файлов и права доступа. Пакет os предоставляет следующие константы для дескрипторов файлов:
const (
// Необходимо указать один из трёх режимов: только чтение, только запись, чтение-запись
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 // открытие файла с синхронным IO
O_TRUNC int = syscall.O_TRUNC // усечение файла при открытии для записи
)Для прав доступа к файлам предоставлены следующие константы:
const (
ModeDir = fs.ModeDir // d: директория
ModeAppend = fs.ModeAppend // a: только добавление
ModeExclusive = fs.ModeExclusive // l: эксклюзивный
ModeTemporary = fs.ModeTemporary // T: временный файл
ModeSymlink = fs.ModeSymlink // L: символическая ссылка
ModeDevice = fs.ModeDevice // D: устройство
ModeNamedPipe = fs.ModeNamedPipe // p: именованный канал (FIFO)
ModeSocket = fs.ModeSocket // S: Unix domain сокет
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: Unix символьное устройство, если установлен ModeDevice
ModeSticky = fs.ModeSticky // t: sticky bit
ModeIrregular = fs.ModeIrregular // ?: нерегулярный файл
// Маска для битов типа. Для обычных файлов ничего не устанавливается.
ModeType = fs.ModeType
ModePerm = fs.ModePerm // Unix права, 0o777
)Ниже приведён пример открытия файла в режиме чтения-записи с правами 0666, что означает, что все пользователи могут читать и записывать файл, и файл будет создан, если он не существует:
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if os.IsNotExist(err) {
fmt.Println("Файл не существует")
} else if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file.Name())
file.Close()
}
}Вывод:
Файл успешно открыт README.txtЕсли нужно получить информацию о файле без его чтения, можно использовать функцию os.Stat():
func main() {
fileInfo, err := os.Stat("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("%+v", fileInfo))
}
}Вывод:
&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}WARNING
После открытия файла всегда нужно закрывать его. Обычно операция закрытия помещается в оператор defer:
defer file.Close()Чтение
После успешного открытия файла можно приступать к чтению. Тип *os.File предоставляет следующие открытые методы для чтения:
// Чтение файла в переданный байтовый срез
func (f *File) Read(b []byte) (n int, err error)
// Чтение с указанием смещения
func (f *File) ReadAt(b []byte, off int64) (n int, err error)В большинстве случаев используется первый метод. Для первого метода необходимо самостоятельно реализовать логику динамического расширения среза при чтении:
func ReadFile(file *os.File) ([]byte, error) {
buffer := make([]byte, 0, 512)
for {
// Когда ёмкость недостаточна
if len(buffer) == cap(buffer) {
// Расширение
buffer = append(buffer, 0)[:len(buffer)]
}
// Продолжение чтения файла
offset, err := file.Read(buffer[len(buffer):cap(buffer)])
// Добавление прочитанных данных в срез
buffer = buffer[:len(buffer)+offset]
// При возникновении ошибки
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
}
return buffer, err
}
}
}Остальная логика:
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file.Name())
bytes, err := ReadFile(file)
if err != nil {
fmt.Println("Ошибка чтения файла", err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}Вывод:
Файл успешно открыт README.txt
hello world!Также можно использовать две удобные функции для чтения файла: ReadFile из пакета os и ReadAll из пакета io. Для os.ReadFile достаточно указать путь к файлу, а для io.ReadAll требуется реализация типа io.Reader.
os.ReadFile
func ReadFile(name string) ([]byte, error)Пример использования:
func main() {
bytes, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}Вывод:
hello world!io.ReadAll
func ReadAll(r Reader) ([]byte, error)Пример использования:
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file.Name())
bytes, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}Файл успешно открыт README.txt
hello world!Запись
Структура os.File предоставляет следующие методы для записи данных:
// Запись байтового среза
func (f *File) Write(b []byte) (n int, err error)
// Запись строки
func (f *File) WriteString(s string) (n int, err error)
// Запись с указанного смещения, при открытии с флагом os.O_APPEND вернёт ошибку
func (f *File) WriteAt(b []byte, off int64) (n int, err error)Для записи в файл необходимо открыть его в режиме O_WRONLY или O_RDWR, иначе запись не удастся. Ниже приведён пример открытия файла в режиме os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC с правами 0666:
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file.Name())
for i := 0; i < 5; i++ {
offset, err := file.WriteString("hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}Поскольку файл открыт в режиме os.O_APPEND, данные будут добавляться в конец файла. После выполнения содержимое файла:
hello world!
hello world!
hello world!
hello world!
hello world!Запись байтовых срезов выполняется аналогично. Стандартная библиотека также предоставляет удобные функции для записи: os.WriteFile и io.WriteString.
os.WriteFile
func WriteFile(name string, data []byte, perm FileMode) errorПример использования:
func main() {
err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
if err != nil {
fmt.Println(err)
}
}Содержимое файла:
hello world!io.WriteString
func WriteString(w Writer, s string) (n int, err error)Пример использования:
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("Ошибка доступа к файлу")
} else {
fmt.Println("Файл успешно открыт", file.Name())
for i := 0; i < 5; i++ {
offset, err := io.WriteString(file, "hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}hello world!
hello world!
hello world!
hello world!
hello world!Функция os.Create используется для создания файлов и по сути является обёрткой вокруг OpenFile:
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}WARNING
При создании файла, если родительская директория не существует, создание завершится ошибкой.
Копирование
Для копирования файла необходимо открыть два файла. Первый способ — прочитать данные из исходного файла и записать их в целевой:
func main() {
// Чтение данных из исходного файла
data, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
return
}
// Запись в целевой файл
err = os.WriteFile("README(1).txt", data, 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Копирование успешно выполнено")
}
}*os.File.ReadFrom
Второй способ — использование метода ReadFrom типа os.File. При открытии один файл открывается только для чтения, другой — только для записи:
func (f *File) ReadFrom(r io.Reader) (n int64, err error)Пример использования:
func main() {
// Открытие исходного файла только для чтения
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// Открытие целевого файла только для записи
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// Чтение данных из исходного файла и запись в целевой
offset, err := target.ReadFrom(origin)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Файл скопирован успешно", offset)
}Этот способ копирования сначала загружает всё содержимое файла в память, а затем записывает в целевой файл, поэтому не рекомендуется использовать его для больших файлов.
io.Copy
Третий способ — использование функции io.Copy, которая читает и записывает одновременно, сначала читая в буфер, затем записывая в целевой файл. Размер буфера по умолчанию — 32 КБ:
func Copy(dst Writer, src Reader) (written int64, err error)Пример использования:
func main() {
// Открытие исходного файла только для чтения
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// Открытие целевого файла только для записи
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// Копирование
written, err := io.Copy(target, origin)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(written)
}
}Также можно использовать io.CopyBuffer для указания размера буфера.
Переименование
Переименование также можно понять как перемещение файла, используется функция Rename из пакета os:
func Rename(oldpath, newpath string) errorПример:
func main() {
err := os.Rename("README.txt", "readme.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Переименование успешно выполнено")
}
}Эта функция также работает для директорий.
Удаление
Операция удаления проще других, используются две функции из пакета os:
// Удаление одного файла или пустой директории, возвращается ошибка если директория не пуста
func Remove(name string) error
// Удаление всех файлов и директорий в указанной директории включая поддиректории и подфайлы
func RemoveAll(path string) errorИспользование очень простое. Пример удаления директории:
func main() {
// Удаление всех файлов и поддиректорий в текущей директории
err := os.RemoveAll(".")
if err != nil {
fmt.Println(err)
}else {
fmt.Println("Удаление успешно выполнено")
}
}Пример удаления одного файла:
func main() {
// Удаление файла в текущей директории
err := os.Remove("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Удаление успешно выполнено")
}
}Синхронизация
Функция os.Sync инкапсулирует системный вызов Fsync для записи кэшированных данных IO на диск:
func main() {
create, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer create.Close()
_, err = create.Write([]byte("hello"))
if err != nil {
panic(err)
}
// Синхронизация с диском
if err := create.Sync();err != nil {
return
}
}Директории
Многие операции с директориями аналогичны операциям с файлами.
Чтение
Для директорий существует два способа открытия:
os.ReadDir
Первый способ — использование функции os.ReadDir:
func ReadDir(name string) ([]DirEntry, error)func main() {
// Текущая директория
dir, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dir {
fmt.Println(entry.Name())
}
}
}*os.File.ReadDir
Второй способ — использование функции *os.File.ReadDir. Функция os.ReadDir является простой обёрткой вокруг *os.File.ReadDir:
// При n < 0 читается всё содержимое директории
func (f *File) ReadDir(n int) ([]DirEntry, error)func main() {
// Текущая директория
dir, err := os.Open(".")
if err != nil {
fmt.Println(err)
}
defer dir.Close()
dirs, err := dir.ReadDir(-1)
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dirs {
fmt.Println(entry.Name())
}
}
}Создание
Для создания директорий используются две функции из пакета os:
// Создание директории с указанным именем и правами
func Mkdir(name string, perm FileMode) error
// Создание директории со всеми необходимыми родительскими директориями
func MkdirAll(path string, perm FileMode) errorПример:
func main() {
err := os.Mkdir("src", 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Создание успешно выполнено")
}
}Копирование
Можно написать функцию для рекурсивного обхода всей директории, но стандартная библиотека filepath уже предоставляет подобные функции, поэтому можно использовать их напрямую. Пример копирования директории:
func CopyDir(src, dst string) error {
// Проверка состояния исходной директории
_, err := os.Stat(src)
if err != nil {
return err
}
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// Вычисление относительного пути
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
// Формирование целевого пути
destpath := filepath.Join(dst, rel)
// Создание директории
var dirpath string
var mode os.FileMode = 0755
if info.IsDir() {
dirpath = destpath
mode = info.Mode()
} else if info.Mode().IsRegular() {
dirpath = filepath.Dir(destpath)
}
if err := os.MkdirAll(dirpath, mode); err != nil {
return err
}
// Создание файла
if info.Mode().IsRegular() {
srcfile, err := os.Open(path)
if err != nil {
return err
}
// Обязательно закройте файл
defer srcfile.Close()
destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer destfile.Close()
// Копирование содержимого файла
if _, err := io.Copy(destfile, srcfile); err != nil {
return err
}
return nil
}
return nil
})
}filepath.Walk рекурсивно обходит всю директорию, создавая директории при встрече папок и создавая новые файлы с копированием при встрече файлов. Код немного сложнее копирования файлов, но не слишком сложный.
