One place for hosting & domains

      ошибок

      Создание настраиваемых ошибок в Go


      Введение

      Go предоставляет два способа создания ошибок в стандартной библиотеке, errors.New и fmt.Errorf. При передаче более сложной информации об ошибках для ваших пользователей или для собственного использования в будущем во время отладки может случиться, что этих двух механизмов будет недостаточно для надлежащего сбора данных и вывода информации о том, что произошло. Чтобы передавать эту более подробную информацию и реализовать дополнительный функционал, мы можем реализовать стандартный тип интерфейса библиотеки — error.

      Синтаксис в этом случае будет выглядеть следующим образом:

      type error interface {
        Error() string
      }
      

      Пакет builtin определяет error как интерфейс с единственным методом Error()​​​, который возвращает сообщение об ошибке в виде строки. При реализации этого метода мы можем преобразовать любой тип, который мы определяем в качестве нашей собственной ошибки.

      Давайте попробуем запустить следующий пример, чтобы увидеть реализацию интерфейса error:

      package main
      
      import (
          "fmt"
          "os"
      )
      
      type MyError struct{}
      
      func (m *MyError) Error() string {
          return "boom"
      }
      
      func sayHello() (string, error) {
          return "", &MyError{}
      }
      
      func main() {
          s, err := sayHello()
          if err != nil {
              fmt.Println("unexpected error: err:", err)
              os.Exit(1)
          }
          fmt.Println("The string:", s)
      }
      

      Вывод должен выглядеть так:

      Output

      unexpected error: err: boom exit status 1

      Здесь мы создали новый пустой тип структуры, MyError, и определили в нем метод Error(). Метод Error() возвращает строку "boom".

      Внутри main() мы вызываем функцию sayHello, которая возвращает пустую строку и новый экземпляр MyError. Поскольку sayHello всегда будет возвращать ошибку, вызов fmt.Println внутри тела оператора if в main() будет выполняться всегда. Затем мы используем fmt.Println для вывода короткого префикса "unexpected error:" вместе с экземпляром MyError, который хранится внутри переменной err.

      Обратите внимание, что нам не нужно напрямую вызывать Error(), поскольку пакет fmt может автоматически обнаруживать, что это реализация error. Он вызывает Error() явно, чтобы получить строку "boom" и выполняет конкатенацию со строкой префикса "unexpected error: err:".

      Сбор подробной информации в настраиваемой ошибке

      Иногда настраиваемая ошибка является самым понятным способом получения подробной информации об ошибке. Например, скажем, мы хотим получать код статуса для ошибок, генерируемых HTTP-запросом; запустите следующую программу, чтобы посмотреть на реализацию error, которая позволяет нам получать эту информацию:

      package main
      
      import (
          "errors"
          "fmt"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              os.Exit(1)
          }
          fmt.Println("success!")
      }
      

      Вывод будет выглядеть следующим образом:

      Output

      status 503: err unavailable exit status 1

      В этом примере мы создаем новый экземпляр RequestError и предоставляем код статуса и ошибку, используя функцию errors.New стандартной библиотеки. Затем мы выводим эту информацию с помощью fmt.Println, как показано в предыдущих примерах.

      Внутри метода Error() в RequestError мы используем функцию fmt.Sprintf для создания строки с информацией, предоставляемой при создании ошибки.

      Утверждение типа и настраиваемые ошибки

      Интерфейс error раскрывает только один метод, но нам может потребоваться доступ к другим методам реализаций error для корректной обработки ошибки. Например, у нас может быть несколько настраиваемых реализаций error, которые имеют временный характер и могут быть использованы повторно, на что указывает наличие метода Temporary().

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

      Следующий пример дополняет пример с RequestError, показанный ранее, и демонстрирует метод Temporary(), который будет указывать, должны ли вызывающие повторять запрос:

      package main
      
      import (
          "errors"
          "fmt"
          "net/http"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return r.Err.Error()
      }
      
      func (r *RequestError) Temporary() bool {
          return r.StatusCode == http.StatusServiceUnavailable // 503
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              re, ok := err.(*RequestError)
              if ok {
                  if re.Temporary() {
                      fmt.Println("This request can be tried again")
                  } else {
                      fmt.Println("This request cannot be tried again")
                  }
              }
              os.Exit(1)
          }
      
          fmt.Println("success!")
      }
      

      Вывод будет выглядеть следующим образом:

      Output

      unavailable This request can be tried again exit status 1

      Внутри main() мы вызываем doRequest(), метод, возвращающий нам интерфейс error. Сначала мы выводим сообщение об ошибке, возвращаемое методом Error(). Далее мы попробуем открыть все методы RequestError, используя утверждение типов re, ok := err.( *RequestError). Если утверждение типа будет выполнено успешно, мы будем использовать метод Temporary(), чтобы убедиться, что эта ошибка является временной ошибкой. Поскольку StatusCode, заданный doRequest(), равен 503, что соответствует http.StatusServiceUnavailable, будет возвращено значение true, а на экран будет выведена причина "This request can be tried again"​​​. На практике мы будем выполнять другой запрос, а не выводить сообщение.

      Обертка для ошибок

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

      Следующий пример показывает, как мы можем привязать некоторую сопроводительную информацию к какой-либо непонятной ошибке, возвращаемой одной из функций:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      type WrappedError struct {
          Context string
          Err     error
      }
      
      func (w *WrappedError) Error() string {
          return fmt.Sprintf("%s: %v", w.Context, w.Err)
      }
      
      func Wrap(err error, info string) *WrappedError {
          return &WrappedError{
              Context: info,
              Err:     err,
          }
      }
      
      func main() {
          err := errors.New("boom!")
          err = Wrap(err, "main")
      
          fmt.Println(err)
      }
      

      Вывод будет выглядеть следующим образом:

      Output

      main: boom!

      WrappedError — это структура с двумя полями: контекстное сообщение, например, в виде строки, и ошибка, о которой WrappedError предоставляет дополнительную информацию. Когда вызывается метод Error(), мы снова будем использовать fmt.Sprintf для вывода контекстного сообщения, а затем error (fmt.Sprintf также неявно вызывает метод Error()).

      Внутри main() мы создаем ошибку, используя errors.New, а затем оборачиваем эту ошибку, используя определенную нами функцию Wrap. Это позволяет нам указать, что эта ошибка была сгенерирована в методе "main". Также, поскольку наша WrappedError также является ошибкой, мы можем обернуть другие ошибки WrappedError, что позволит нам посмотреть цепочку, чтобы мы могли отследить источник ошибки. При небольшой помощи стандартной библиотеки мы можем даже ввести полноценную трассировку стека для наших ошибок.

      Заключение

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

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



      Source link

      Обработка ошибок в Go


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

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

      Создание ошибок

      Прежде чем мы сможем обработать ошибку, нам нужно ее создать. Стандартная библиотека предоставляет две встроенные функции для создания ошибок: errors.New и fmt.Errorf. Обе эти функции позволяют нам указывать настраиваемое сообщение об ошибке, которое вы можете отображать вашим пользователям.

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

      Попробуйте запустить следующий пример, чтобы увидеть ошибку, созданную с помощью errors.New, которая выполняет стандартный вывод:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func main() {
          err := errors.New("barnacles")
          fmt.Println("Sammy says:", err)
      }
      

      Output

      Sammy says: barnacles

      Мы использовали функцию errors.New из стандартной библиотеки для создания нового сообщения об ошибке со строкой "barnacles" в качестве сообщения об ошибке. Мы выполняли требование конвенции, используя строчные буквы для сообщения об ошибке, как показано в руководстве по стилю для языка программирования Go.

      Наконец, мы использовали функцию fmt.Println для объединения сообщения о ошибке со строкой "Sammy says:".

      Функция fmt.Errorf позволяет динамически создавать сообщение об ошибке. Ее первый аргумент — это строка, которая содержит ваше сообщение об ошибке с заполнителями, такими как %s для строки и %d для целых чисел. fmt.Errorf интерполирует аргументы, которые находятся за этой форматированной строкой, на эти заполнители по порядку:

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          err := fmt.Errorf("error occurred at: %v", time.Now())
          fmt.Println("An error happened:", err)
      }
      

      Output

      An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

      Мы использовали функцию fmt.Errorf для создания сообщения об ошибке, которое будет включать текущее время. Форматированная строка, которую мы предоставили fmt.Errorf, содержит директиву форматирования %v, которая указывает fmt.Errorf использовать формат по умолчанию для первого аргумента, предоставленного после форматированной строки. Этот аргумент будет текущим временем, предоставленным функцией time.Now из стандартной библиотеки. Как и в предыдущем примере, мы добавляем в сообщение об ошибке короткий префикс и выводим результат стандартным образом, используя fmt.Println.

      Обработка ошибок

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

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

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func boom() error {
          return errors.New("barnacles")
      }
      
      func main() {
          err := boom()
      
          if err != nil {
              fmt.Println("An error occurred:", err)
              return
          }
          fmt.Println("Anchors away!")
      }
      

      Output

      An error occurred: barnacles

      Здесь мы определяем функцию под именем boom(), которая возвращает error, которую мы создаем с помощью errors.New. Затем мы вызываем эту функцию и захватываем ошибку в строке err := boom(). После получения этой ошибки мы проверяем, присутствует ли она, с помощью условия if err ! = nil. Здесь условие всегда выполняет оценку на true, поскольку мы всегда возвращаем error из boom().

      Это не всегда так, поэтому лучше использовать логику, обрабатывающую случаи, когда ошибка отсутствует (nil) и случаи, когда ошибка есть. Когда ошибка существует, мы используем fmt.Println для вывода ошибки вместе с префиксом, как мы делали в предыдущих примерах. Наконец, мы используем оператор return, чтобы пропустить выполнение fmt.Println("Anchors away!"), поскольку этот код следует выполнять только при отсутствии ошибок.

      Примечание: конструкция if err !​​​ = nil, показанная в последнем примере, является стандартной практикой обработки ошибок в языке программирования Go. Если функция может генерировать ошибку, важно использовать оператор if, чтобы проверить наличие ошибки. Таким образом, код Go естественным образом имеет логику “happy path”на первом уровне условия и логику “sad path”на втором уровне условия.

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

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

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func boom() error {
          return errors.New("barnacles")
      }
      
      func main() {
          if err := boom(); err != nil {
              fmt.Println("An error occurred:", err)
              return
          }
          fmt.Println("Anchors away!")
      }
      

      Output

      An error occurred: barnacles

      Как и ранее, у нас есть функция boom(), которая всегда возвращает ошибку. Мы присвоим ошибку, возвращаемую boom(), переменной err в первой части оператора if. Эта переменная err будет доступна во второй части оператора if после точки с запятой. Мы должны убедиться в наличии ошибки и вывести нашу ошибку с коротким префиксом, как мы уже делали до этого.

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

      Возврат ошибок вместе со значениями

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

      Чтобы создать функцию, которая возвращает несколько значений, мы перечислим типы всех возвращаемых значений внутри скобок в сигнатуре функции. Например, функция capitalize, которая возвращает string и error, будет объявлена следующим образом: func capitalize(name string) (string, error) {}. Часть (string, error) сообщает компилятору Go, что эта функция возвращает строку и ошибку в указанном порядке.

      Запустите следующую программу, чтобы увидеть вывод функции, которая возвращает string и error:

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, error) {
          if name == "" {
              return "", errors.New("no name provided")
          }
          return strings.ToTitle(name), nil
      }
      
      func main() {
          name, err := capitalize("sammy")
          if err != nil {
              fmt.Println("Could not capitalize:", err)
              return
          }
      
          fmt.Println("Capitalized name:", name)
      }
      

      Output

      Capitalized name: SAMMY

      Мы определяем capitalize() как функцию, которая принимает строку (имя, которое нужно указать с большой буквы) и возвращает строку и значение ошибки. В main() мы вызываем capitalize() и присваиваем два значения, возвращаемые функцией, для переменных name и err, разделив их запятой с левой стороны оператора :=. После этого мы выполняем нашу проверку if err ! = nil, как показано в предыдущих примерах, используя стандартный вывод и fmt.Println, если ошибка присутствует. Если ошибок нет, мы выводим Capitalized name: SAMMY.

      Попробуйте изменить строку "sammy" в name, err := capitalize("sammy")​​​ на пустую строку ("") и получите вместо этого ошибку Could not capitalize: no name provided.

      Функция capitalize возвращает ошибку, когда вызов функции предоставляет пустую строку в качестве параметра name. Когда параметр name не является пустой строкой, capitalize() использует strings.ToTitle для замены строчных букв на заглавные для параметра name и возвращает nil для значения ошибки.

      Существует несколько конвенций, которым следует этот пример и которые типичны для Go, но не применяются компилятором Go. Когда функция возвращает несколько значений, включая ошибку, конвенция просит, чтобы мы возвращали error последним элементом. При возвращении ошибки функцией с несколькими возвращаемыми значениями, идиоматический код Go также устанавливает для любого значения, не являющегося ошибкой, нулевое значение. Нулевое значение — это, например, пустая строка для string, 0 для целых чисел, пустая структура для структур и nil для интерфейса и типов указателя и т. д. Мы более подробно познакомимся с нулевыми значениями в нашем руководстве по переменным и константам.

      Сокращение шаблонного кода

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

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

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, int, error) {
          handle := func(err error) (string, int, error) {
              return "", 0, err
          }
      
          if name == "" {
              return handle(errors.New("no name provided"))
          }
      
          return strings.ToTitle(name), len(name), nil
      }
      
      func main() {
          name, size, err := capitalize("sammy")
          if err != nil {
              fmt.Println("An error occurred:", err)
          }
      
          fmt.Printf("Capitalized name: %s, length: %d", name, size)
      }
      

      Output

      Capitalized name: SAMMY, length: 5

      Внутри main() мы получим три возвращаемых аргумента из capitalize: name, size и err. Затем мы проверим, возвращает ли capitalize error, убедившись, что переменная err не равна nil. Это важно сделать, прежде чем пытаться использовать любое другое значение, возвращаемое capitalize, поскольку анонимная функция handle может задать для них нулевые значения. Поскольку ошибок не возникает, потому что мы предоставили строку ​​​"sammy"​​​, мы выведем состоящее из заглавных букв имя и его длину.

      Вы снова можете попробовать заменить "sammy" на пустую строку ("") и увидеть ошибку (An error occurred: no name provided).

      Внутри capitalize мы определяем переменную handle как анонимную функцию. Она получает одну ошибку и возвращает идентичные значения в том же порядке, что и значения, возвращаемые capitalize. handle задает для них нулевые значения и перенаправляет error, переданную в качестве аргумента, как конечное возвращаемое значение. Таким образом мы можем вернуть любые ошибки, возникающие в capitalize, с помощью оператора return перед вызовом handle с error в качестве параметра.

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

      Обработка ошибок функций с несколькими возвращаемыми значениями

      Когда функция возвращает множество значений, Go требует, чтобы каждое из них было привязано к переменной. В последнем примере мы делали это, указав имена двух значений, возвращаемых функцией capitalize. Эти имена должны быть разделены запятыми и отображаться слева от оператора :=. Первое значение, возвращаемое capitalize, будет присвоено переменной name, а второе значение (error) будет присваиваться переменной err. Бывает, что нас интересует только значение ошибки. Вы можете пропустить любые нежелательные значения, которые возвращает функция, с помощью специального имени переменной _.

      В следующей программе мы изменили наш первый пример с функцией capitalize для получения ошибки, передав функции пустую строку (""). Попробуйте запустить эту программу, чтобы увидеть, как мы можем изучить только ошибку, убрав первое возвращаемое значение с переменной _:

      package main
      
      import (
          "errors"
          "fmt"
          "strings"
      )
      
      func capitalize(name string) (string, error) {
          if name == "" {
              return "", errors.New("no name provided")
          }
          return strings.ToTitle(name), nil
      }
      
      func main() {
          _, err := capitalize("")
          if err != nil {
              fmt.Println("Could not capitalize:", err)
              return
          }
          fmt.Println("Success!")
      }
      

      Output

      Could not capitalize: no name provided

      Внутри функции main() на этот раз мы присвоим состоящее из заглавных букв имя (строка, возвращаемая первой) переменной с нижним подчеркиванием (_). В то же самое время мы присваиваем error, которую возвращает capitalize, переменной err. Теперь мы проверим, существует ли ошибка в if err ! = nil. Поскольку мы жестко задали пустую строку как аргумент для capitalize в строке _, err := capitalize(""), это условие всегда будет равно true. В результате мы получим вывод "Could not capitalize: no name provided" при вызове функции fmt.Println в теле условия if. Оператор return после этого будет пропускать fmt.Println("Success!").

      Заключение

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



      Source link