Skip to content

Файлы

Стандартные библиотеки Go для обработки файлов включают:

  • os — реализация взаимодействия с файловой системой ОС
  • io — абстрактный уровень для чтения и записи IO
  • fs — абстрактный уровень файловой системы

В этой статье мы рассмотрим основы обработки файлов в Go.

Открытие

Два распространённых способа открытия файла — использование функций Open и OpenFile из пакета os. Функция Open возвращает указатель на файл и ошибку:

go
func Open(name string) (*File, error)

Функция OpenFile предоставляет более детальное управление, а функция Open является простой обёрткой вокруг OpenFile:

go
func OpenFile(name string, flag int, perm FileMode) (*File, error)

Рассмотрим первый способ — просто указываем имя файла:

go
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 предоставляет удобные функции для этого:

go
func main() {
  file, err := os.Open("README.txt")
  if os.IsNotExist(err) {
    fmt.Println("Файл не существует")
  } else if err != nil {
    fmt.Println("Ошибка доступа к файлу")
  } else {
    fmt.Println("Файл успешно открыт", file)
  }
}

Повторный запуск выведет:

Файл не существует

Первый способ чтения файла поддерживает только режим только для чтения, файл нельзя модифицировать:

go
func Open(name string) (*File, error) {
  return OpenFile(name, O_RDONLY, 0)
}

Функция OpenFile позволяет контролировать больше деталей, таких как дескрипторы файлов и права доступа. Пакет os предоставляет следующие константы для дескрипторов файлов:

go
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  // усечение файла при открытии для записи
)

Для прав доступа к файлам предоставлены следующие константы:

go
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, что означает, что все пользователи могут читать и записывать файл, и файл будет создан, если он не существует:

go
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():

go
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:

go
defer file.Close()

Чтение

После успешного открытия файла можно приступать к чтению. Тип *os.File предоставляет следующие открытые методы для чтения:

go
// Чтение файла в переданный байтовый срез
func (f *File) Read(b []byte) (n int, err error)

// Чтение с указанием смещения
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

В большинстве случаев используется первый метод. Для первого метода необходимо самостоятельно реализовать логику динамического расширения среза при чтении:

go
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
    }
  }
}

Остальная логика:

go
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

go
func ReadFile(name string) ([]byte, error)

Пример использования:

go
func main() {
  bytes, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(string(bytes))
  }
}

Вывод:

hello world!

io.ReadAll

go
func ReadAll(r Reader) ([]byte, error)

Пример использования:

go
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 предоставляет следующие методы для записи данных:

go
// Запись байтового среза
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:

go
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, данные будут добавляться в конец файла. После выполнения содержимое файла:

txt
hello world!
hello world!
hello world!
hello world!
hello world!

Запись байтовых срезов выполняется аналогично. Стандартная библиотека также предоставляет удобные функции для записи: os.WriteFile и io.WriteString.

os.WriteFile

go
func WriteFile(name string, data []byte, perm FileMode) error

Пример использования:

go
func main() {
  err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
  if err != nil {
    fmt.Println(err)
  }
}

Содержимое файла:

txt
hello world!

io.WriteString

go
func WriteString(w Writer, s string) (n int, err error)

Пример использования:

go
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:

go
func Create(name string) (*File, error) {
   return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

WARNING

При создании файла, если родительская директория не существует, создание завершится ошибкой.

Копирование

Для копирования файла необходимо открыть два файла. Первый способ — прочитать данные из исходного файла и записать их в целевой:

go
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. При открытии один файл открывается только для чтения, другой — только для записи:

go
func (f *File) ReadFrom(r io.Reader) (n int64, err error)

Пример использования:

go
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 КБ:

go
func Copy(dst Writer, src Reader) (written int64, err error)

Пример использования:

go
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:

go
func Rename(oldpath, newpath string) error

Пример:

go
func main() {
  err := os.Rename("README.txt", "readme.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("Переименование успешно выполнено")
  }
}

Эта функция также работает для директорий.

Удаление

Операция удаления проще других, используются две функции из пакета os:

go
// Удаление одного файла или пустой директории, возвращается ошибка если директория не пуста
func Remove(name string) error

// Удаление всех файлов и директорий в указанной директории включая поддиректории и подфайлы
func RemoveAll(path string) error

Использование очень простое. Пример удаления директории:

go
func main() {
  // Удаление всех файлов и поддиректорий в текущей директории
  err := os.RemoveAll(".")
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("Удаление успешно выполнено")
  }
}

Пример удаления одного файла:

go
func main() {
  // Удаление файла в текущей директории
  err := os.Remove("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("Удаление успешно выполнено")
  }
}

Синхронизация

Функция os.Sync инкапсулирует системный вызов Fsync для записи кэшированных данных IO на диск:

go
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:

go
func ReadDir(name string) ([]DirEntry, error)
go
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:

go
// При n < 0 читается всё содержимое директории
func (f *File) ReadDir(n int) ([]DirEntry, error)
go
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:

go
// Создание директории с указанным именем и правами
func Mkdir(name string, perm FileMode) error

// Создание директории со всеми необходимыми родительскими директориями
func MkdirAll(path string, perm FileMode) error

Пример:

go
func main() {
  err := os.Mkdir("src", 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("Создание успешно выполнено")
  }
}

Копирование

Можно написать функцию для рекурсивного обхода всей директории, но стандартная библиотека filepath уже предоставляет подобные функции, поэтому можно использовать их напрямую. Пример копирования директории:

go
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 рекурсивно обходит всю директорию, создавая директории при встрече папок и создавая новые файлы с копированием при встрече файлов. Код немного сложнее копирования файлов, но не слишком сложный.

Golang by www.golangdev.cn edit