Рассмотрите фрагмент на Go: func save(data []byte) { f, _ := os.Create("out") defer f.Close() f.Write(data) } — объясните последствия игнорирования ошибки при создании файла, какие классы ошибок пропускаются, и предложите идиоматичные паттерны обработки ошибок и восстановления (retry, контекст, возврат ошибки вверх по стеку)
Кратко и по делу. Последствия игнорирования ошибки в фрагменте func save(data []byte) { f, _ := os.Create("out") defer f.Close() f.Write(data) } - Если os.Create вернул ошибку, то f будет nil → вызов f.Close() или f.Write(...) приведёт к panic (nil pointer dereference). - Игнорирование ошибки скрывает причину отказа и мешает корректной обработке/логированию/повторным попыткам. - Также пропускается проверка на ошибку при Close() (она тоже может вернуть ошибку записи/файловой системы). Какие классы ошибок пропускаются - Постоянные (не-retryable): право доступа (EACCES) — проверяется через os.IsPermission(err); некорректный путь/имя — os.IsNotExist(err) илиEINVAL; файловая система только для чтения. - Временные/транзиентные (можно пробовать повторно): сетевые/удалённые mount (NFS) с временными IO-ошибками; исчерпание дескрипторов (EMFILE) или временные перегрузки; временные ошибки ввода-вывода. - Квоты/место: ENOSPC — можно считать «временной» в зависимости от сценария (пока не освободят место — не поможет). - Для точной классификации используйте errors.As/Is и, при необходимости, сравнение с syscall.Errno. Идиоматичные паттерны обработки и восстановления 1) Простое и правильное — вернуть ошибку вверх (propagate) и проверять Close: func save(data []byte) error { f, err := os.Create("out") if err != nil { return fmt.Errorf("create out: %w", err) } defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = fmt.Errorf("close failed: %w", cerr) } }() _, err = f.Write(data) return err } 2) Возврат с обёрткой ошибок — использовать %w: return fmt.Errorf("save out: %w", err) 3) Retry + backoff + проверка retryable: - Ограничение числа попыток, например maxAttempts =3=3=3. - Экспоненциальный backoff: backoff=b⋅2n\text{backoff} = b \cdot 2^{n}backoff=b⋅2n (b — базовая задержка, n — номер попытки), добавить jitter. - Пропускать повтор для временных ошибок, не повторять для os.IsPermission/os.IsNotExist и т.п. Пример (упрощённо): func saveWithRetry(ctx context.Context, data []byte) error { var lastErr error const maxAttempts = 3 base := 100 * time.Millisecond for i := 0; i < maxAttempts; i++ { select { case <-ctx.Done(): return ctx.Err() default: } f, err := os.Create("out") if err == nil { _, err = f.Write(data) _ = f.Close() } if err == nil { return nil } lastErr = err // 判断可重试: например, если временная ошибка или errno == EMFILE/ETIMEDOUT и т.д. if !isRetryable(err) { return fmt.Errorf("save failed: %w", err) } // backoff с экспонентой и jitter time.Sleep(time.Duration(math.Pow(2, float64(i)))*base + jitter()) } return fmt.Errorf("save failed after retries: %w", lastErr) } Функция isRetryable должна использовать errors.As/Is и/или анализ syscall.Errno и сетевых ошибок. 4) Контекст (cancellation, timeout) - Принимайте context.Context в API: func save(ctx context.Context, data []byte) error. - Прерывайте попытки при ctx.Done(). Это позволяет внешнему коду управлять тайм-аутом/отменой. Рекомендации-резюме - Никогда не игнорируйте ошибку от os.Create. Проверяйте err прежде чем использовать f и прежде чем defer f.Close(). - Возвращайте и оборачивайте ошибки, пусть вызывающий код решает (логировать/пытаться снова/фаталить). - Для восстановления используйте retry с экспоненциальным backoff и jitter, но пропускайте повторы для явно непоправимых ошибок. - Обрабатывайте ошибку Close() отдельно (она может содержать важную информацию о записи).
Последствия игнорирования ошибки в фрагменте
func save(data []byte) {
f, _ := os.Create("out")
defer f.Close()
f.Write(data)
}
- Если os.Create вернул ошибку, то f будет nil → вызов f.Close() или f.Write(...) приведёт к panic (nil pointer dereference).
- Игнорирование ошибки скрывает причину отказа и мешает корректной обработке/логированию/повторным попыткам.
- Также пропускается проверка на ошибку при Close() (она тоже может вернуть ошибку записи/файловой системы).
Какие классы ошибок пропускаются
- Постоянные (не-retryable): право доступа (EACCES) — проверяется через os.IsPermission(err); некорректный путь/имя — os.IsNotExist(err) илиEINVAL; файловая система только для чтения.
- Временные/транзиентные (можно пробовать повторно): сетевые/удалённые mount (NFS) с временными IO-ошибками; исчерпание дескрипторов (EMFILE) или временные перегрузки; временные ошибки ввода-вывода.
- Квоты/место: ENOSPC — можно считать «временной» в зависимости от сценария (пока не освободят место — не поможет).
- Для точной классификации используйте errors.As/Is и, при необходимости, сравнение с syscall.Errno.
Идиоматичные паттерны обработки и восстановления
1) Простое и правильное — вернуть ошибку вверх (propagate) и проверять Close:
func save(data []byte) error {
f, err := os.Create("out")
if err != nil {
return fmt.Errorf("create out: %w", err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close failed: %w", cerr)
}
}()
_, err = f.Write(data)
return err
}
2) Возврат с обёрткой ошибок — использовать %w:
return fmt.Errorf("save out: %w", err)
3) Retry + backoff + проверка retryable:
- Ограничение числа попыток, например maxAttempts =3=3=3.
- Экспоненциальный backoff: backoff=b⋅2n\text{backoff} = b \cdot 2^{n}backoff=b⋅2n (b — базовая задержка, n — номер попытки), добавить jitter.
- Пропускать повтор для временных ошибок, не повторять для os.IsPermission/os.IsNotExist и т.п.
Пример (упрощённо):
func saveWithRetry(ctx context.Context, data []byte) error {
var lastErr error
const maxAttempts = 3
base := 100 * time.Millisecond
for i := 0; i < maxAttempts; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
f, err := os.Create("out")
if err == nil {
_, err = f.Write(data)
_ = f.Close()
}
if err == nil {
return nil
}
lastErr = err
// 判断可重试: например, если временная ошибка или errno == EMFILE/ETIMEDOUT и т.д.
if !isRetryable(err) {
return fmt.Errorf("save failed: %w", err)
}
// backoff с экспонентой и jitter
time.Sleep(time.Duration(math.Pow(2, float64(i)))*base + jitter())
}
return fmt.Errorf("save failed after retries: %w", lastErr)
}
Функция isRetryable должна использовать errors.As/Is и/или анализ syscall.Errno и сетевых ошибок.
4) Контекст (cancellation, timeout)
- Принимайте context.Context в API: func save(ctx context.Context, data []byte) error.
- Прерывайте попытки при ctx.Done(). Это позволяет внешнему коду управлять тайм-аутом/отменой.
Рекомендации-резюме
- Никогда не игнорируйте ошибку от os.Create. Проверяйте err прежде чем использовать f и прежде чем defer f.Close().
- Возвращайте и оборачивайте ошибки, пусть вызывающий код решает (логировать/пытаться снова/фаталить).
- Для восстановления используйте retry с экспоненциальным backoff и jitter, но пропускайте повторы для явно непоправимых ошибок.
- Обрабатывайте ошибку Close() отдельно (она может содержать важную информацию о записи).