JavaRush /Курсы /Go SELF /Классы файловых ошибок — not exist, permission, invalid p...

Классы файловых ошибок — not exist, permission, invalid path

Go SELF
39 уровень , 4 лекция
Открыта

1. Зачем делить файловые ошибки на классы

Когда вы начинаете работать с файлами, первая реакция на любую проблему обычно выглядит так: «Ой, не открылось — ну и ладно». Но очень быстро оказывается, что почему не открылось — важнее, чем сам факт. Файл мог не существовать (и это нормально при первом запуске), могли быть проблемы с правами (и это уже нужно объяснить пользователю), а мог быть вообще некорректный путь (и это повод чинить входные данные или конфигурацию).

В Go крайне вредно «распознавать» ошибки по тексту (err.Error()), потому что текст зависит от ОС, локали и конкретного системного вызова. Важная идея языка — ошибки являются значениями и часто несут структурную информацию, а не только строку. Поэтому мы учимся определять класс ошибки (not exist / permission / invalid path) программно, а не «по ощущениям».

Для ориентира можно держать в голове простую таблицу:

Класс ошибки Что это значит по-человечески Типичное действие программы
not exist
«Файла/директории нет» Создать файл/директорию или принять “пустое состояние”
permission
«Нет прав читать/писать» Показать понятное сообщение, подсказать путь/права
invalid path
«Путь некорректен» Исправить входные данные/конфиг, а не “ретраить”

2. Класс not exist: файла нет — и это часто нормально

Ошибка «не существует» — самая дружелюбная из файловых ошибок. Она часто означает не катастрофу, а то, что программа запущена впервые, данных ещё нет, директория не создана, или пользователь указал неправильный путь. Важно научиться отличать этот сценарий от «сломалось что-то серьёзное», потому что реакция на них должна быть разной.

В Go для этого есть удобная проверка os.IsNotExist(err). Она умеет работать даже если ошибка обёрнута в несколько слоёв, при условии что вы оборачивали её через %w. Это прямо поддерживает идею «добавляй контекст, но не ломай распознавание причины».

Мини-пример: “если файла нет — считаем, что список задач пустой”

Ниже маленький фрагмент для нашего учебного приложения «мини-todo», где задачи хранятся в файле (по одной задаче на строку). Если файла нет — возвращаем пустой список и nil-ошибку.


package main

import (
	"os"
)

func loadTasks(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil && os.IsNotExist(err) {
		return []byte{}, nil
	}
	return b, err
}

Обратите внимание на стиль: мы не говорим «ошибки нет», мы говорим «ошибка ожидаемая и трактуется как пустые данные».

3. Класс permission: «файл есть, но вам нельзя»

Ошибки прав доступа обычно болезненнее, потому что программа вроде бы всё делает правильно, файл вроде бы существует, но ОС говорит: «нет». И это логично: операционная система защищает данные. На практике причина может быть банальной (запуск без нужных прав, файл принадлежит другому пользователю, директория закрыта на запись), но для пользователя это всегда выглядит как «ваша программа сломалась».

В Go этот класс удобно проверять через os.IsPermission(err). Идея здесь в том, чтобы не писать “что-то пошло не так”, а дать человеку понятное сообщение: “нет прав на чтение/запись по пути …”. Технические подробности можно оставить в обёрнутой ошибке, но наружу (особенно в CLI) стоит показывать коротко и по делу.

Мини-пример: попытка записи и вежливое сообщение

package main

import (
	"fmt"
	"os"
)

func saveTasks(path string, data []byte) error {
	err := os.WriteFile(path, data, 0644)
	if err != nil && os.IsPermission(err) {
		return fmt.Errorf("нет прав записать файл %q: %w", path, err)
	}
	return err
}

Здесь мы одновременно делаем две вещи: отличаем класс permission и добавляем контекст (какой файл, какая операция). Такая «двухслойность» — нормальная практика Go: понятный контекст снаружи, распознаваемая причина внутри.

4. Класс invalid path: неочевидный, но важный

С invalid path есть неприятная новость: в отличие от not exist и permission, он не всегда распознаётся одной универсальной функцией вроде os.IsInvalidPath. Причина простая: что именно считается «некорректным путём» — зависит от ОС и файловой системы. Где-то запрещены одни символы, где-то другие; где-то путь может быть «слишком длинным», где-то — содержать недопустимую последовательность.

Но хорошая новость тоже есть: как минимум, вы должны уметь понять, что проблема именно в пути, и не пытаться лечить её созданием директорий, ретраями и прочей магией. invalid path — это обычно баг входных данных/конфигурации, а не временный сбой.

Практичный “железобетонный” пример некорректного пути: \x00

Есть один трюк, который почти везде работает одинаково: нулевой байт в пути (\x00). Он недопустим для системных вызовов.

package main

import (
	"fmt"
	"os"
)

func main() {
	_, err := os.ReadFile("\x00")
	fmt.Printf("err=%v\n", err) // err=open <...>: invalid argument (формулировка зависит от ОС)
}

Текст будет отличаться на разных системах, и это нормально. Главная мысль: даже если вам кажется, что «путь же строка», ОС воспринимает его как строго ограниченный формат.

5. Почему нельзя сравнивать ошибки по err.Error()

Соблазн понятный: “ну я же вижу текст permission denied, значит просто проверю strings.Contains”. Проблема в том, что это ломается сразу, как только вы меняете окружение. На другой ОС текст другой. На той же ОС, но при другой операции — тоже может быть другой. Даже формат может отличаться: иногда путь включён в сообщение, иногда нет.

Go прямо подталкивает вас к тому, чтобы вы работали с ошибками как со значениями: оборачивали их для контекста и проверяли причину через errors.Is/errors.As или функции os.IsNotExist/os.IsPermission. Это одна из базовых инженерных привычек в экосистеме Go.

Чтобы “увидеть” правильный порядок действий, полезно представлять себе такой протокол обработки:

flowchart TD
    A["Делаем файловую операцию (ReadFile/WriteFile/OpenFile)"] --> B{err == nil?}
    B -->|да| C["Продолжаем работу"]
    B -->|нет| D{"os.IsNotExist(err)?"}
    D -->|да| E["Сценарий 'первый запуск': пустые данные / создать директорию"]
    D -->|нет| F{"os.IsPermission(err)?"}
    F -->|да| G["Сообщение пользователю: нет прав + путь"]
    F -->|нет| H["Считаем ошибку неожиданной: оборачиваем и возвращаем выше"]

6. Полезные нюансы: *os.PathError и wrapping %w

Когда вы печатаете ошибку от os.ReadFile или os.Open, она часто выглядит так, будто в ней уже есть всё: операция, путь, причина. Но для программы это тоже важно: иногда вы хотите не просто вывести “ошибка”, а понять, какая операция упала (open, read, stat), с каким путём, и что за причина внутри.

Для этого в Go часто используется структурная ошибка *os.PathError. Она хранит поля Op, Path, Err. И приятный момент: даже если вы обернули ошибку через %w, вы всё равно можете извлечь *os.PathError через errors.As. Это прямое продолжение “ошибки как значения, а не строки”.

Мини-пример: извлекаем PathError для диагностики

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	_, err := os.ReadFile("no_such_file.txt")
	if err == nil {
		return
	}

	var pe *os.PathError
	if errors.As(err, &pe) {
		fmt.Println(pe.Op)   // open
		fmt.Println(pe.Path) // no_such_file.txt
		fmt.Println(pe.Err)  // конкретная причина (зависит от ОС)
	}
}

Это не про “красивый вывод пользователю”, а про диагностику: вы как разработчик быстро понимаете, что именно случилось.

Когда вы пишете файловый код, ошибку почти всегда нужно возвращать выше “с человеческим пояснением”: что делали и с каким путём. И тут легко выстрелить себе в ногу: если обернуть ошибку через %v, распознавание причины ломается; если обернуть через %w, распознавание сохраняется, но наружу “протекают” детали внутренней ошибки.

В прикладном коде (особенно в приложениях) чаще всего полезно сохранять причину, чтобы верхний уровень мог принять решение. Это как раз и делает %w. Такая модель “контекст снаружи, причина внутри” — стандартная для современного Go.

Мини-пример: “правильное” оборачивание

package main

import (
	"fmt"
	"os"
)

func load(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("load todo file %q: %w", path, err)
	}
	return b, nil
}

Если позже вы сделаете os.IsNotExist(err) или errors.Is(err, os.ErrNotExist), оно продолжит работать.

7. Пример: мини‑todo с классификацией ошибок

Теперь соберём идеи в один небольшой сценарий. Пусть у нас есть файл data/todo/items.txt. Наша программа пытается прочитать задачи, а затем (для простоты) просто печатает, сколько задач найдено. Нам важно, чтобы:

  • если файла нет, это считалось пустым списком;
  • если нет прав, мы сказали это по-человечески;
  • если путь некорректный, мы не делали вид, что “файла просто нет”.

Шаг 1: чтение с классификацией

package main

import (
	"errors"
	"fmt"
	"os"
)

func readTodoFile(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err == nil {
		return b, nil
	}
	if os.IsNotExist(err) {
		return []byte{}, nil
	}
	if os.IsPermission(err) {
		return nil, fmt.Errorf("нет прав на чтение %q: %w", path, err)
	}
	return nil, fmt.Errorf("не удалось прочитать %q: %w", path, err)
}

func main() {
	_, err := readTodoFile("data/todo/items.txt")
	if err != nil && errors.Is(err, os.ErrPermission) {
		fmt.Println("permission problem") // пример ветки (детали выше)
	}
}

Да, мы чуть “перестраховываемся” в main, но это нормально для учебного примера: показываем, что обёртка сохраняет причину.

Шаг 2: человекочитаемая реакция без “простыней”

Ниже — пример функции, которая превращает техническую ошибку в короткое сообщение. Тут важно не впадать в крайности: не прятать ошибку совсем, но и не грузить пользователя трассировкой из трёх экранов.

package main

import (
	"fmt"
	"os"
)

func formatFileError(path string, err error) string {
	if os.IsNotExist(err) {
		return fmt.Sprintf("файл или директория не найдены: %s", path)
	}
	if os.IsPermission(err) {
		return fmt.Sprintf("нет прав для доступа к файлу: %s", path)
	}
	return fmt.Sprintf("ошибка работы с файлом %s: %v", path, err)
}

Здесь мы осознанно оставляем третью ветку общей. Почему? Потому что invalid path и многие другие причины могут выглядеть по-разному, а наша цель — дать пользователю понятный результат, не притворяясь, что мы всё классифицировали идеально.

8. Типичные ошибки

Ошибка №1: проверять типовые файловые проблемы через err.Error() и strings.Contains.
Такой код кажется рабочим ровно до первой смены ОС или до первой ситуации, когда текст ошибки выглядит иначе. Гораздо надёжнее мыслить классами: os.IsNotExist, os.IsPermission, а для диагностики — errors.As(err, &pe) к *os.PathError.

Ошибка №2: оборачивать ошибку через %v, а потом удивляться, что os.IsNotExist “перестало работать”.
Если вам важна распознаваемая причина, используйте %w. В противном случае вы оставляете от причины только строку, а строка, как мы уже выяснили, — штука капризная и платформозависимая.

Ошибка №3: трактовать not exist как “фатальную” ошибку во всех сценариях.
Для многих приложений отсутствие файла данных — нормальное состояние при первом запуске. Если вы сразу падаете с ошибкой, вы делаете программу “хрупкой”: она не умеет стартовать в чистой среде.

Ошибка №4: пытаться “лечить” permission созданием директорий и повторными попытками.
Права — не временный глюк сети. Если ОС говорит «нельзя», обычно нужно либо менять путь, либо запускать с другими правами, либо менять права на файловой системе. Ретраи здесь чаще всего только раздражают.

Ошибка №5: путать “файл не найден” и “путь некорректный”.
Это разные миры: “не найден” может означать, что нужно создать файл; “некорректный путь” чаще означает проблему входных данных или конфигурации. Полезно помнить: invalid path — это не повод создавать директории “на всякий случай”, а повод посмотреть, что за строка пришла в вашу файловую функцию.

1
Задача
Go SELF, 39 уровень, 4 лекция
Недоступна
Сенсор хранилища
Сенсор хранилища
1
Задача
Go SELF, 39 уровень, 4 лекция
Недоступна
Запись пропуска
Запись пропуска
1
Задача
Go SELF, 39 уровень, 4 лекция
Недоступна
Умное загрузчик
Умное загрузчик
1
Задача
Go SELF, 39 уровень, 4 лекция
Недоступна
Разбор полётов
Разбор полётов
1
Опрос
Файлы, 39 уровень, 4 лекция
Недоступен
Файлы
Файлы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ