Если вам нравится читать статьи на Medium и вы заинтересованы в том, чтобы стать участником, я буду рад поделиться с вами своей реферальной ссылкой!



Обработка ошибок является важной частью написания надежного программного обеспечения, и подход Go к обработке ошибок является одной из его определяющих характеристик. Однако, хотя простота обработки ошибок в Go хвалят, она также оставляет место для потенциальных ловушек. Одной из наиболее распространенных ошибок является обработка ошибки более одного раза, что может вызвать путаницу, избыточное ведение журнала и усложнить отладку.

Принцип: обрабатывать ошибки один раз

Идея обработки ошибок только один раз проста: когда возникает ошибка, вы должны решить, где ее обрабатывать, и делать это там и только там. Эта обработка может включать регистрацию ошибки, возвращение ошибки дальше по стеку или, возможно, даже завершение программы, в зависимости от серьезности ошибки.

Однако чего следует избегать, так это обработки ошибки в нескольких местах. Распространенная ошибка заключается в том, чтобы зарегистрировать ошибку и вернуть ее. Это может показаться хорошей идеей, поскольку обеспечивает немедленную обратную связь в журналах, а также позволяет вызывающей стороне обработать ошибку. Однако на практике это может привести к путанице и избыточности.

Проблема двойной обработки

Рассмотрим параллельную программу с несколькими горутинами. Если каждая горутина регистрирует ошибку, а затем возвращает ее, сообщения журнала и возвращаемые ошибки могут не соответствовать друг другу. Поскольку горутины выполняются одновременно, их сообщения журнала могут чередоваться, что затрудняет определение того, какое сообщение журнала соответствует какой ошибке.

Более того, логирование ошибки в момент сбоя и ее возврат приводит к дублированию в логах. Одна и та же ошибка может быть зарегистрирована несколько раз, так как она всплывает в стеке, что затрудняет чтение журналов и занимает место впустую.

В этом примере у нас есть пять горутин, каждая из которых выполняет задачу с разной продолжительностью. Задачи с четным идентификатором задачи рассчитаны на неудачу. Мы регистрируем ошибку сразу же при сбое задачи, а также передаем ее в канал.

При такой настройке порядок сообщений журнала и ошибок, полученных из канала, может отличаться из-за одновременного выполнения и разной продолжительности задач, что приводит к путанице при попытке сопоставить сообщения журнала с полученными ошибками. Это подчеркивает, почему обычно лучше обрабатывать ошибки в одном месте либо путем регистрации, либо путем возврата, но не одновременно.

package main

import (
 "fmt"
 "log"
 "sync"
 "time"
)

func main() {
 var wg sync.WaitGroup
 errorsCh := make(chan error, 5)

 for i := 1; i <= 5; i++ {
  wg.Add(1)
  go func(taskID int) {
   defer wg.Done()
   err := performTask(taskID)
   if err != nil {
    log.Println(err)
    errorsCh <- err
   }
  }(i)
 }

 wg.Wait()
 close(errorsCh)

 log.Printf("Performed %d tasks", 5-len(errorsCh))

 for err := range errorsCh {
  fmt.Println("Received error:", err)
 }
}

func performTask(taskID int) error {
 // Simulate varying task duration with potential errors
 time.Sleep(time.Duration(taskID) * time.Second)

 if taskID%2 == 0 {
  return fmt.Errorf("task %d failed", taskID)
 }
 return nil
}

И запутанная избыточная регистрация:

Решение: упаковка ошибок

Go 1.13 представил решение этой проблемы: перенос ошибок. Обтекание ошибок позволяет вернуть ошибку с дополнительным контекстом, не регистрируя ошибку. Глагол %w с fmt.Errorf используется для создания новой ошибки, которая дополняет исходную ошибку дополнительной информацией. Затем эта обернутая ошибка может быть возвращена вверх по стеку.

Вот пример:

package main

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

var ErrFileOpen = errors.New("failed to open the file")

func OpenFile(filename string) error {
 _, err := os.Open(filename)
 if err != nil {
  // Wrap the original error with additional context
  return fmt.Errorf("%w: %s", ErrFileOpen, filename)
 }
 return nil
}

func main() {
 err := OpenFile("nonexistent.txt")
 if err != nil {
  fmt.Println(err)
 }
}

В этом примере, если os.Open терпит неудачу, мы создаем новую ошибку, которая включает исходную ошибку и имя файла, который нам не удалось открыть. Затем мы возвращаем эту обернутую ошибку. Затем функция main регистрирует ошибку. Это гарантирует, что каждая ошибка обрабатывается (и регистрируется) ровно один раз, что позволяет избежать проблем с двойной обработкой.

Заключение

Правильная обработка ошибок жизненно важна для создания надежных приложений Go. Один из самых важных принципов, которого следует придерживаться, — обрабатывать каждую ошибку ровно один раз. Двойная обработка, например запись ошибки в журнал и последующий ее возврат, может привести к путанице в журналах и затруднить отладку. Вместо этого рассмотрите возможность использования переноса ошибок, чтобы предоставить больше контекста об ошибке, и пусть вызывающая сторона решает, как обработать ошибку. Это приведет к более чистой и удобной обработке ошибок в ваших программах Go.