Показаны сообщения с ярлыком обработка ошибок в Go. Показать все сообщения
Показаны сообщения с ярлыком обработка ошибок в Go. Показать все сообщения

вторник, 30 июня 2020 г.

Пакет errors в Golang

Пакет errors реализует функции для манипулирования ошибками.

Функция New создает ошибки, единственным содержимым которых является текстовое сообщение.

Функции Unwrap, Is и As работают с ошибками, которые могут переносить другие ошибки. Ошибка переносит другую ошибку, если ее тип имеет метод

Unwrap() error

Если e.Unwrap() возвращает ненулевую ошибку w, то мы говорим, что e переносит w.

Unwrap распаковывает упакованные ошибки. Если тип его аргумента имеет метод Unwrap, он вызывает метод один раз. В противном случае возвращается nil.

Простой способ создать упакованные ошибки - вызвать fmt.Errorf и применить глагол %w к аргументу ошибки:

errors.Unwrap(fmt.Errorf("... %w ...", ..., err, ...))

возвращает err.

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

if errors.Is(err, os.ErrExist)

предпочтительнее чем

if err == os.ErrExist

потому что первый преуспеет, если err обернет os.ErrExist.

As разворачивает свой первый аргумент последовательно ищет ошибку, которая может быть назначена второму аргументу, который должен быть указателем. Если это успешно, выполняется назначение и возвращает true. В противном случае возвращается false. Форма

var perr *os.PathError
if errors.As(err, &perr) {
    fmt.Println(perr.Path)
}

предпочтительнее чем

if perr, ok := err.(*os.PathError); ok {
    fmt.Println(perr.Path)
}

потому что первый будет успешным, если err обернет *os.PathError.

Пример использования пользовательского типа ошибки

package main

import (
    "fmt"
    "time"
)

// MyError это реализация error 
// которая включает время и сообщение.
type MyError struct {
    When time.Time
    What string
}

func (e MyError) Error() string {
    return fmt.Sprintf("%v: %v", e.When, e.What)
}

func oops() error {
    return MyError{
        time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
        "the file system has gone away",
    }
}

func main() {
    if err := oops(); err != nil {
        fmt.Println(err)
    }
}

Вывод:

1989-03-15 22:30:00 +0000 UTC: the file system has gone away

Функция New

func New(text string) error

New возвращает ошибку, которая форматируется как заданный текст. Каждый вызов New возвращает отдельное значение ошибки, даже если текст идентичен.

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("Неожиданная ошибка")
    if err != nil {
        fmt.Print(err)
    }
}

Вывод:

Неожиданная ошибка

Функция Errorf пакета fmt позволяет использовать функции форматирования пакета для создания описательных сообщений об ошибках.

package main

import (
    "fmt"
)

func main() {
    const name, id = "бублик", 117
    err := fmt.Errorf("пользователь %q (id %d) не найден", name, id)
    if err != nil {
        fmt.Print(err)
    }
}

Вывод:

пользователь "бублик" (id 117) не найден

Функция Is (с версии Go 1.13)

func Is(err, target error) bool

Is сообщает, соответствует ли target ошибке какая-либо ошибка в цепочке err.

Цепочка состоит из самой err, за которой следует последовательность ошибок, получаемых повторным вызовом Unwrap.

Считается, что ошибка соответствует цели, если она равна этой цели или если она реализует метод Is(error) bool, такой что Is(target) возвращает true.

Тип ошибки может предоставлять метод Is, поэтому его можно рассматривать как эквивалент существующей ошибки. Например, если MyError определяет

func (m MyError) Is(target error) bool { return target == os.ErrExist }

затем Is (MyError{}, os.ErrExist) возвращает значение true. syscall.Errno.Is - пример в стандартной библиотеке.

Функция As (с версии Go 1.13)

func As(err error, target interface{}) bool

As находит первую ошибку в цепочке err, которая соответствует target, и если находит, то устанавливает target равным этому значению ошибки и возвращает true. В противном случае возвращается false.

Цепочка состоит из самой err, за которой следует последовательность ошибок, получаемых повторным вызовом Unwrap.

Ошибка соответствует цели, если конкретное значение ошибки присваивается значению, на которое указывает цель, или если ошибка имеет метод As(interface{}) bool, такой что As(target) возвращает true. В последнем случае метод As отвечает за установку цели.

Тип ошибки может предоставлять метод As, поэтому его можно обрабатывать так, как если бы это был другой тип ошибки.

As паникует, если target не является ненулевым указателем ни на тип, который реализует error, ни на любой тип интерфейса.

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

package main

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

func main() {
    if _, err := os.Open("non-existing"); err != nil {
        var pathError *os.PathError
        if errors.As(err, &pathError) {
            fmt.Println("Failed at path:", pathError.Path)
        } else {
            fmt.Println(err)
        }
    }

}

Вывод:

Failed at path: non-existing

Функция Unwrap

func Unwrap(err error) error

Unwrap возвращает результат вызова метода Unwrap для err, если тип err содержит метод Unwrap, возвращающий ошибку. В противном случае Unwrap возвращает nil.


Читайте также:


пятница, 1 мая 2020 г.

Три простых способа создать ошибку в Golang

Строковые ошибки

Стандартная библиотека предлагает два готовых варианта.

// простая строковая ошибка
err1 := errors.New("math: square root of negative number")

// с форматированием
err2 := fmt.Errorf("math: square root of negative number %g", x)

Пользовательские ошибки с данными

Чтобы определить пользовательский тип ошибки, вы должны реализовать предварительно объявленный интерфейс error.

type error interface {
    Error() string
}

Вот два примера.

type SyntaxError struct {
    Line int
    Col  int
}

func (e *SyntaxError) Error() string {
    return fmt.Sprintf("%d:%d: syntax error", e.Line, e.Col)
}

type InternalError struct {
    Path string
}

func (e *InternalError) Error() string {
    return fmt.Sprintf("parse %v: internal error", e.Path)
}

Если Foo является функцией, которая может возвращать SyntaxError или InternalError, вы можете обрабатывать два случая, как показано ниже:

if err := Foo(); err != nil {
    switch e := err.(type) {
    case *SyntaxError:
        // Делаем что-нибудь интересное с e.Line и e.Col.
    case *InternalError:
        // Прервать и записать проблему.
    default:
        log.Println(e)
    }
}


Читайте также:


пятница, 14 февраля 2020 г.

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

Если вы писали какой-либо код на Go, вы, вероятно, сталкивались со встроенным типом error. Код Go использует значения error, чтобы указать ненормальное состояние. Например, функция os.Open возвращает ненулевое значение error, когда не удается открыть файл.

func Open(name string) (file *File, err error)

Следующий код использует os.Open для открытия файла. Если возникает ошибка, она вызывает log.Fatal, чтобы распечатать сообщение об ошибке и остановить исполнение.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// делаем что-либо с открытым *File f

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

Тип error

Тип error - это тип интерфейса. Переменная error представляет любое значение, которое может быть описано как строка. Вот объявление интерфейса:

type error interface {
    Error() string
}

Тип error, как и для всех встроенных типов, предварительно объявлен в блоке юниверса (universe block).

Наиболее часто используемая реализация error - это неэкспортируемый тип errorString пакета errors.

// errorString это тривиальная реализация error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Вы можете создать одно из этих значений с помощью функции errors.New. Он принимает строку, которая преобразуется в error.errorString и возвращает значение error.

// New возвращает ошибку, которая форматируется как заданный текст.
func New(text string) error {
    return &errorString{text}
}

Вот как вы можете использовать errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // реализация
}

Вызывающая сторона, передающая отрицательный аргумент в Sqrt, получает ненулевое значение ошибки (конкретное представление которого является значением errors.errorString). Вызывающая сторона может получить доступ к строке ошибки ("math: square root of..."), вызвав метод Error ошибки или просто распечатав его:

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

Пакет fmt форматирует значение ошибки, вызывая его строковый метод Error().

Ответственность за реализацию ошибки заключается в обобщении контекста. Ошибка, возвращаемая форматом os.Open как "open /etc/passwd: permission denied", а не просто "permission denied". Ошибка, возвращаемая нашим Sqrt, содержит информацию о недопустимом аргументе.

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

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

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

Например, наши гипотетические пользователи могут захотеть восстановить неверный аргумент, переданный Sqrt. Мы можем включить это, определив новую реализацию ошибок вместо использования errors.errorString:

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

Сложный вызывающий объект может затем использовать утверждение типа, чтобы проверить наличие NegativeSqrtError и обработать его специально, в то время как вызывающие элементы, которые просто передают ошибку в fmt.Println или log.Fatal, не увидят никаких изменений в поведении.

В качестве другого примера, пакет json указывает тип SyntaxError, который функция json.Decode возвращает, когда сталкивается с синтаксической ошибкой при разборе BLOB-объекта JSON.

type SyntaxError struct {
    msg    string // описание ошибки
    Offset int64  // ошибка произошла после чтения Offset байтов
}

func (e *SyntaxError) Error() string { return e.msg }

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

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

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

package net

type Error interface {
    error
    Timeout() bool   // ошибка истекла?
    Temporary() bool // ошибка временная?
}

Клиентский код может проверить net.Error с утверждением типа, а затем отличить временные сетевые ошибки от постоянных. Например, веб-сканер может перевести в спящий режим и повторить попытку, если он обнаружит временную ошибку, и в противном случае откажется.

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Упрощение повторяющейся обработки ошибок

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

Рассмотрим приложение App Engine с обработчиком HTTP, который извлекает запись из хранилища данных и форматирует ее с помощью шаблона.

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Эта функция обрабатывает ошибки, возвращаемые функцией datastore.Get и методом Execute viewTemplate. В обоих случаях он представляет пользователю простое сообщение об ошибке с кодом состояния HTTP 500 ("Internal Server Error"). Это похоже на управляемый объем кода, но добавьте еще несколько обработчиков HTTP, и вы быстро получите много копий идентичного кода обработки ошибок.

Чтобы уменьшить количество повторений, мы можем определить наш собственный тип HTTP appHandler, который включает возвращаемое значение ошибки:

type appHandler func(http.ResponseWriter, *http.Request) error

Затем мы можем изменить нашу функцию viewRecord, чтобы она возвращала ошибки:

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

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

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Метод ServeHTTP вызывает функцию appHandler и отображает возвращенную ошибку (если есть) пользователю. Обратите внимание, что получатель метода, fn, является функцией. (Go может сделать это!) Метод вызывает функцию, вызывая получателя в выражении fn(w, r).

Теперь при регистрации viewRecord в пакете http мы используем функцию Handle (вместо HandleFunc), так как appHandler является http.Handler (а не http.HandlerFunc).

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

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

Для этого мы создаем структуру appError, содержащую ошибку и некоторые другие поля:

type appError struct {
    Error   error
    Message string
    Code    int
}

Далее мы модифицируем тип appHandler, чтобы он возвращал значения *appError:

type appHandler func(http.ResponseWriter, *http.Request) *appError

(Обычно является ошибкой возвращать конкретный тип ошибки, а не error, но в данном случае это правильно, потому что ServeHTTP - единственное место, которое видит значение и использует его содержимое.)

И изменяем метод appHandler ServeHTTP, чтобы он отображал сообщение appError для пользователя с правильным кодом состояния HTTP и регистрировал полную ошибку на консоли разработчика:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e это *appError, а не os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

Наконец, мы обновляем viewRecord на новую сигнатуру функции и возвращаем больше контекста при обнаружении ошибки:

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

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

Это не конец; мы можем еще больше улучшить обработку ошибок в нашем приложении. Вот некоторые идеи:

  • дать обработчику ошибок красивый HTML-шаблон,
  • упростить отладку, записав трассировку стека в ответ HTTP, когда пользователь является администратором,
  • написать функцию конструктора для appError, которая хранит трассировку стека для упрощения отладки,
  • recover (восстанавливаться) от panic внутри appHandler, записав ошибку на консоль как "Critical" и сообщив пользователю "произошла серьезная ошибка". Это удобно, чтобы не подвергать пользователя непостижимым сообщениям об ошибках, вызванным ошибками программирования.

Заключение

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


Читайте также:


среда, 30 октября 2019 г.

Работа с ошибками в Go 1.13

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

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string { 
    return e.Query + ": " + e.Err.Error() 
}

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

Шаблон одной ошибки, содержащей другую, настолько распространен в коде Go, что после всестороннего обсуждения в Go 1.13 добавлена явная поддержка. В этом посте описываются дополнения к стандартной библиотеке, обеспечивающие эту поддержку: три новые функции в пакете errors и новый глагол форматирования для fmt.Errorf.

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

Ошибки до Go 1.13


Проверки ошибок

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

if err != nil {
    // что-то пошло не так
}

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

var ErrNotFound = errors.New("not found")

if err == ErrNotFound {
    // что-то не найдено
}

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

type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string { 
    return e.Name + ": not found" 
}

if e, ok := err.(*NotFoundError); ok {
    // e.Name не найдено
}

Добавление информации

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

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

Создание новой ошибки с помощью fmt.Errorf удаляет все из исходной ошибки, кроме текста. Как мы видели выше с QueryError, иногда мы можем захотеть определить новый тип ошибки, который содержит основную ошибку, сохранив ее для проверки кодом. Опять QueryError:

type QueryError struct {
    Query string
    Err   error
}

Программы могут заглянуть внутрь значения *QueryError, чтобы принимать решения на основе подлежащей ошибки. Иногда вы встретите, что это называется "разворачиванием" ("unwrapping") ошибки.

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // запрос не выполнен из-за проблем с правами доступа
}

Тип os.PathError в стандартной библиотеке является одним из примеров ошибки, которая содержит другую.

Ошибки в Go 1.13


Метод Unwrap

Go 1.13 представляет новые функции для errors и fmt пакетов стандартной библиотеки для упрощения работы с ошибками, которые содержат другие ошибки. Наиболее важным из них является соглашение, а не изменение: ошибка, которая содержит другую ошибку, может реализовать метод Unwrap, возвращающий основную ошибку. Если e1.Unwrap() возвращает e2, то мы говорим, что e1 оборачивает e2 (e1 wraps e2), и вы можете развернуть e1 (unwrap e1), чтобы получить e2.

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

func (e *QueryError) Unwrap() error { return e.Err }

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

Проверка ошибок с Is и As

Пакет errors в Go 1.13 включает две новые функции для проверки ошибок: Is и As.

Функция errors.Is сравнивает ошибку со значением.

// Похоже на:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // что-то не найдено
}

Функция As проверяет, является ли ошибка определенным типом.

// Похоже на:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
    // err является *QueryError, 
    // а в e устанавливается значение ошибки
}

В простейшем случае функция errors.Is ведет себя как сравнение с дозорной ошибкой, а error.As действует как утверждение типа. Однако при работе с обернутыми ошибками эти функции учитывают все ошибки в цепочке. Еще раз посмотрим на приведенный выше пример развертывания QueryError, чтобы изучить основную ошибку:

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // запрос не выполнен из-за проблем с правами доступа
}

Используя функцию errors.Is, мы можем записать это как:

if errors.Is(err, ErrPermission) {
    // err, или какая-то ошибка, которую она содержит, 
    // является ошибкой прав доступа
}

Пакет errors также включает новую функцию Unwrap, которая возвращает результат вызова метода Unwrap ошибки или nil, если у ошибки нет метода Unwrap. Обычно лучше использовать errors.Is или errors.As, поскольку эти функции будут проверять всю цепочку за один вызов.

Оборачивание ошибок с %w

Как упоминалось ранее, обычно используется функция fmt.Errorf для добавления дополнительной информации об ошибке.

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

В Go 1.13 функция fmt.Errorf поддерживает новый глагол %w. Когда этот глагол присутствует, ошибка, возвращаемая fmt.Errorf, будет иметь метод Unwrap, возвращающий аргумент %w, который должен быть ошибкой. В остальном, %w идентичен %v.

if err != nil {
    // Возвращаем ошибку, которая разворачивается в err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

Оборачивание ошибки с %w делает ее доступной для errors.Is и errors.As:

err := fmt.Errorf("access denied: %w”, ErrPermission)
...
if errors.Is(err, ErrPermission) ...

Оборачивать ли ошибки?

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

В качестве примера представим функцию Parse, которая считывает сложную структуру данных из io.Reader. Если возникает ошибка, мы хотим сообщить номер строки и столбца, на котором она произошла. Если ошибка возникает при чтении из io.Reader, мы хотим обернуть эту ошибку, чтобы разрешить проверку основной проблемы. Так как вызывающая сторона предоставила функции io.Reader, имеет смысл показать ошибку, вызванную ею.

Напротив, функция, которая делает несколько обращений к базе данных, вероятно, не должна возвращать ошибку, которая разворачивается в результате одного из этих вызовов. Если база данных, используемая функцией, является деталью реализации, то раскрытие этих ошибок является нарушением абстракции. Например, если функция LookupUser вашего пакета pkg использует пакет Go database/sql, то может возникнуть ошибка sql.ErrNoRows. Если вы вернете эту ошибку с помощью fmt.Errorf("accessing DB: %v", err), то вызывающая сторона не сможет заглянуть внутрь, чтобы найти sql.ErrNoRows. Но если функция вместо этого возвращает fmt.Errorf("accessing DB: %w", err), то вызывающая сторона может разумно написать

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …

На этом этапе функция всегда должна возвращать sql.ErrNoRows, если вы не хотите ломать своих клиентов, даже если вы переключаетесь на другой пакет базы данных. Другими словами, оборачивание ошибки делает эту ошибку частью вашего API. Если вы не хотите поддерживать эту ошибку как часть вашего API в будущем, вам не следует оборачивать ошибку.

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

Настройка тестов ошибок с помощью методов Is и As

Функция errors.Is проверяет каждую ошибку в цепочке на соответствие целевому значению. По умолчанию ошибка соответствует цели, если они равны. Кроме того, ошибка в цепочке может объявить, что она соответствует цели путем реализации метода Is.

В качестве примера рассмотрим следующую ошибку, вдохновленную пакетом ошибок Upspin, который сравнивает ошибку с шаблоном, рассматривая только поля, отличные от нуля в шаблоне:

type Error struct {
    Path string
    User string
}

func (e *Error) Is(target error) bool {
    t, ok := target.(*Error)
    if !ok {
        return false
    }
    return (e.Path == t.Path || t.Path == "") &&
           (e.User == t.User || t.User == "")
}

if errors.Is(err, &Error{User: "someuser"}) {
    // поле User в err равно "someuser".
}

Функция errors.As аналогично обращается к методу As при его наличии.

Ошибки и пакеты API

Пакет, который возвращает ошибки (а большинство возвращает) должен описывать, на какие свойства этих ошибок могут положиться программисты. Хорошо разработанный пакет также позволит избежать возврата ошибок со свойствами, на которые нельзя полагаться.

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

Если мы хотим, чтобы функция возвращала идентифицируемое условие ошибки, такое как "item not found", мы могли бы вернуть ошибку, оборачивающую дозорную ошибку.

var ErrNotFound = errors.New("not found")

// FetchItem возвращает именованный элемент.
//
// Если элемент с таким именем не существует, 
// FetchItem возвращает ошибку оборачивающую ErrNotFound.
func FetchItem(name string) (*Item, error) {
    if itemNotFound(name) {
        return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
    }
    // ...
}

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

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

f, err := os.Open(filename)
if err != nil {
    // *os.PathError возвращенная от os.Open 
    // это внутренняя деталь. Чтобы не показывать ее 
    // вызывающей стороне, упакем ее как новую ошибку
    // с тем же текстом. 
    // Используем глагол форматирования %v, так как
    // %w позволит вызывающей стороне 
    // развернуть исходную *os.PathError.
    return fmt.Errorf("%v", err)
}

Если функция определена как возвращающая ошибку, заключающую в себе какую-то дозорную ошибку или тип, не возвращайте основную ошибку напрямую.

var ErrPermission = errors.New("permission denied")
// DoSomething возвращает ошибку, 
// оборачивающую ErrPermission, если пользователь
// не имеет прав доступа к чему-либо.
func DoSomething() {
    if !userHasPermission() {
        // Если возвращаем ErrPermission напрямую, 
        // вызывающая сторона может прийти
        // к зависимости от точного значения ошибки, 
        // написав подобный код:
        //
        //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
        //
        // Это может вызвать проблемы 
        // если мы хотим добавить дополнительный
        // контекст к ошибке в будущем. 
        // Чтобы избежать этого возвращаем ошибку,
        // оборачивающую дозорную ошибку,
        // так что пользователи всегда 
        // должны развернуть ее:
        //
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

Заключение

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


Читайте также:


воскресенье, 3 марта 2019 г.

Go Code Review Comments: Отступы в ответвлениях ошибок

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

if err != nil {
    // обработка ошибки
} else {
    // нормальный код
}

Вместо этого напишите:

if err != nil {
    // обработка ошибки
    return // или continue, и т.д.
}
// нормальный код

Если в операторе if есть оператор инициализации, например:

if x, err := f(); err != nil {
    // обработка ошибки
    return
} else {
    // использование x
}

тогда для этого может потребоваться переместить краткое объявление переменной в отдельную строку:

x, err := f()
if err != nil {
    // обработка ошибки
    return
}
// использование x


Читайте также:


суббота, 2 марта 2019 г.

Go Code Review Comments: Внутренние ошибки

В C и аналогичных языках функции обычно возвращают значения, такие как -1 или ноль, чтобы сигнализировать об ошибках или пропущенных результатах:

// Lookup возвращает значение для ключа или "", 
// если нет соответствия для ключа.
func Lookup(key string) string

// Невозможность проверить значение внутренней ошибки 
// может привести к багам:
Parse(Lookup(key))  
// возвращает "parse failure for value" 
// вместо "no value for key" 

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

// Lookup возвращает значение для ключа или ok = false, 
// если для ключа нет соответствия.
func Lookup(key string) (value string, ok bool)

Это препятствует тому, чтобы вызывающая сторона использовала результат неправильно:

Parse(Lookup(key))  // compile-time error

И поощряет более надежный и читаемый код:

value, ok := Lookup(key)
if !ok  {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

Это правило применяется к экспортируемым функциям, но также полезно для неэкспортируемых функций.

Возвращаемые значения, такие как nil, "", 0 и -1, хороши, когда они являются действительными результатами для функции, то есть когда вызывающей стороне нет необходимости обрабатывать их иначе, чем другие значения.

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


Читайте также:


четверг, 28 февраля 2019 г.

Go Code Review Comments: комментарии к документу, panic, строки ошибок, примеры

Комментарии к документу (Doc comments)

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

Не используйте panic напрасно

Не используйте панику для нормальной обработки ошибок. Используйте ошибку и несколько возвращаемых значений.

Строки ошибок

Строки ошибок не должны начинаться с заглавной буквы (если только они не начинаются с собственных имен или аббревиатур) или заканчиваться пунктуацией, поскольку они обычно печатаются в другом контексте. То есть используйте fmt.Errorf("something bad") , а не fmt.Errorf("Something bad"), так что log.Printf("Reading %s: %v", filename, err) форматируется без ложной заглавной буквы в середине сообщения. Это не относится к сообщениям журнала (logging), которые ориентированы на использование сообщения как отдельной строки и не объединяются внутри других сообщений.

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

Не отбрасывайте ошибки, используя пустые переменные _. Если функция возвращает ошибку, проверьте ее, чтобы убедиться, что функция выполнена успешно. Обработайте ошибку, верните ее или, в действительно исключительных ситуациях, используйте panic.

Примеры

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


Читайте также: