Golang new error handling

Handling errors is an essential feature of solid code. In this section, you'll add a bit of code to return an error from the greetings module, then handle it in the caller.

Handling errors is an essential feature of solid code. In this section, you’ll
add a bit of code to return an error from the greetings module, then handle it
in the caller.

  1. In greetings/greetings.go, add the code highlighted below.

    There’s no sense sending a greeting back if you don’t know who to greet.
    Return an error to the caller if the name is empty. Copy the following
    code into greetings.go and save the file.

    package greetings
    
    import (
        "errors"
        "fmt"
    )
    
    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return "", errors.New("empty name")
        }
    
        // If a name was received, return a value that embeds the name
        // in a greeting message.
        message := fmt.Sprintf("Hi, %v. Welcome!", name)
        return message, nil
    }
    

    In this code, you:

    • Change the function so that it returns two values: a
      string and an error. Your caller will check
      the second value to see if an error occurred. (Any Go function can
      return multiple values. For more, see
      Effective Go.)
    • Import the Go standard library errors package so you can
      use its
      errors.New function.
    • Add an if statement to check for an invalid request (an
      empty string where the name should be) and return an error if the
      request is invalid. The errors.New function returns an
      error with your message inside.
    • Add nil (meaning no error) as a second value in the
      successful return. That way, the caller can see that the function
      succeeded.
  2. In your hello/hello.go file, handle the error now returned by the
    Hello function, along with the non-error value.

    Paste the following code into hello.go.

    package main
    
    import (
        "fmt"
        "log"
    
        "example.com/greetings"
    )
    
    func main() {
        // Set properties of the predefined Logger, including
        // the log entry prefix and a flag to disable printing
        // the time, source file, and line number.
        log.SetPrefix("greetings: ")
        log.SetFlags(0)
    
        // Request a greeting message.
        message, err := greetings.Hello("")
        // If an error was returned, print it to the console and
        // exit the program.
        if err != nil {
            log.Fatal(err)
        }
    
        // If no error was returned, print the returned message
        // to the console.
        fmt.Println(message)
    }
    

    In this code, you:

    • Configure the
      log package to
      print the command name («greetings: «) at the start of its log messages,
      without a time stamp or source file information.
    • Assign both of the Hello return values, including the
      error, to variables.
    • Change the Hello argument from Gladys’s name to an empty
      string, so you can try out your error-handling code.
    • Look for a non-nil error value. There’s no sense continuing
      in this case.
    • Use the functions in the standard library’s log package to
      output error information. If you get an error, you use the
      log package’s
      Fatal function
      to print the error and stop the program.
  3. At the command line in the hello directory, run hello.go to
    confirm that the code works.

    Now that you’re passing in an empty name, you’ll get an error.

    $ go run .
    greetings: empty name
    exit status 1
    

That’s common error handling in Go: Return an error as a value so the caller
can check for it.

Next, you’ll use a Go slice to return a randomly-selected greeting.

< Call your code from another module
Return a random greeting >

title

Буквально пару дней назад в Денвере закончилась очередная, уже 5-я по счёту, крупнейшая конференция по Go – GopherCon. На ней команда Go сделала важное заявление – черновики предварительного дизайна новой обработки ошибок и дженериков в Go 2 опубликованы, и все приглашаются к обсуждению.

Я постараюсь подробно пересказать суть этих черновиков в трёх статьях.

Как многим, наверняка, известно, в прошлом году (также на GopherCon) команда Go объявила, что собирает отчёты (experience reports) и предложения для решения главных проблем Go – тех моментов, которые по опросам собирали больше всего критики. В течении года все предложения и репорты изучались и рассматривались, и помогли в создании черновиков дизайна, о которых и будет идти речь.

Итак, начнём с черновиков нового механизма обработки ошибок.

Для начала, небольшое отступление:

  1. Go 2 это условное название – все нововведения будут частью обычного процесса выпуска версий Go. Так что пока неизвестно, будет ли это Go 1.34 или Go2. Сценария Python 2/3 не будет железно.
  2. Черновики дизайна это даже не предложения (proposals), с которых начинается любое изменение в библиотеке, тулинге или языке Go. Это начальная точка для обсуждения дизайна, предложенная командой Go после нескольких лет работы над данными вопросами. Всё, что описано в черновиках с большой долей вероятности будет изменено, и, при наилучших раскладах, воплотится в реальность только через несколько релизов (я даю ~2 года).

В чём проблема с обработкой ошибок в Go?

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

Возьмём пример гипотетического Go с исключениями:

func CopyFile(src, dst string) throws error {
    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    io.Copy(w, r)
    w.Close()
}

Это приятный, чистый и элегантный код. Он также некорректый: если io.Copy или w.Close завершатся неудачей, данный код не удалит созданный и недозаписанный файл.

С другой стороны, код на реальном Go выглядит так:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return err
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer w.Close()

    if _, err := io.Copy(w, r); err != nil {
        return err
    }
    if err := w.Close(); err != nil {
        return err
    }
}

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

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

Проще говоря, в Go слишком много проверки ошибок и недостаточно обработки ошибок. Более полноценная версия кода выше будет выглядеть вот так:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

Исправление проблем сделало код корректным, но никак не чище или элегантней.

Цели

Команда Go ставит перед собой следующие цели для улучшения обработки ошибок в Go 2:

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

Черновик дизайна предлагает изменить или дополнить семантику обработки ошибок в Go.

Дизайн

Предложенный дизайн вводит две новых синтаксические формы.

  • check(x,y,z) или check err обозначающую явную проверку ошибки
  • handle – определяющую код, обрабатывающий ошибки

Если check возвращает ошибку, то контроль передаётся в ближайший блок handle (который передаёт контроль в следущий по лексическому контексту handler, если такой есть, и. затем, вызывает return)

Код выше будет выглядеть так:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    handle err {
        w.Close()
        os.Remove(dst) // (только если check упадёт)
    }

    check io.Copy(w, r)
    check w.Close()
    return nil
}

Этот синтаксис разрешён также в функциях, которые не возвращают ошибки (например main). Следующая программа:

func main() {
    hex, err := ioutil.ReadAll(os.Stdin)
    if err != nil {
        log.Fatal(err)
    }

    data, err := parseHexdump(string(hex))
    if err != nil {
        log.Fatal(err)
    }

    os.Stdout.Write(data)
}

может быть переписана как:

func main() {
    handle err {
        log.Fatal(err)
    }

    hex := check ioutil.ReadAll(os.Stdin)
    data := check parseHexdump(string(hex))
    os.Stdout.Write(data)
}

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

func printSum(a, b string) error {
    x, err := strconv.Atoi(a)
    if err != nil {
        return err
    }
    y, err := strconv.Atoi(b)
    if err != nil {
        return err
    }
    fmt.Println("result:", x + y)
    return nil
}

может быть переписан как:

func printSum(a, b string) error {
    handle err { return err }
    x := check strconv.Atoi(a)
    y := check strconv.Atoi(b)
    fmt.Println("result:", x + y)
    return nil
}

или даже вот так:

func printSum(a, b string) error {
    handle err { return err }
    fmt.Println("result:", check strconv.Atoi(x) + check strconv.Atoi(y))
    return nil
}

Давайте рассмотрим подробнее детали предложенных конструкций check и handle.

Check

check это (скорее всего) ключевое слово, которое чётко выражает действие «проверка» и применяется либо к переменной типа error, либо к функции, возвращающую ошибку последним значением. Если ошибка не равна nil, то check вызывает ближайший обработчик(handler), и вызывает return с результатом обработчика.

Следующий пример:

v1, ..., vN := check <выражение>

равнозначен этому коду:

v1, ..., vN, vErr := <выражение>
if vErr != nil {
    <error result> = handlerChain(vn)
    return
}

где vErr должен иметь тип error и <error result> означает ошибку, возвращённую из обработчика.

Аналогично,

foo(check <выражение>)

равнозначно:

v1, ..., vN, vErr := <выражение>
if vErr != nil {
    <error result> = handlerChain(vn)
    return
}
foo(v1, ..., vN)

Check против try

Изначально пробовали слово try вместо check – оно более популярное/знакомое, и, например, Rust и Swift используют try (хотя Rust уходит в пользу постфикса ? уже).

try неплохо читался с функциями:

data := try parseHexdump(string(hex))

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

data, err := parseHexdump(string(hex))
if err == ErrBadHex {
    ... special handling ...
}
try err

Кроме того, try всё таки несёт багаж cхожести с механизмом исключений и может вводить в заблуждение. Поскольку предложенный дизайн check/handle существенно отличается от исключений, выбор явного и красноречивого слова check кажется оптимальным.

Handle

handle описывает блок, называемый «обработчик» (handler), который будет обрабатывать ошибку, переданную в check. Возврат (return) из этого блока означает незамедлительный выход из функции с текущими значениями возвращаемых переменных. Возврат без переменных (то есть, просто return) возможен только в функциях с именованными переменными возврата (например func foo() (bar int, err error)).

Поскольку обработчиков может быть несколько, формально вводится понятие «цепочки обработчиков» – каждый из них это, по сути, функция, которая принимает на вход переменную типа error и возвращает те же самые переменные, что и функция, для которой обработчик определяется. Но семантика обработчика может быть описана вот так:

func handler(err error) error {...}

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

Порядок обработчиков

Важный момент для понимания – в каком порядке будут вызываться обработчики, если их несколько. Каждая проверка (check) может иметь разные обработчики, в зависимости от скопа, в котором они вызываются. Первым будет вызван обработчик, который ближе всего объявлен в текущем скопе, вторым – следующий в обратном порядке объявления. Вот пример для лучшего понимания:

func process(user string, files chan string) (n int, err error) {
    handle err { return 0, fmt.Errorf("process: %v", err)  }      // handler A
    for i := 0; i < 3; i++ {
        handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
        handle err { err = moreWrapping(err) }                    // handler C

        check do(something())  // check 1: handler chain C, B, A
    }
    check do(somethingElse())  // check 2: handler chain A
}

Проверка check 1 вызовет обработчики C, B и A – именно в таком порядке. Проверка check 2 вызовет только A, так как C и B были определены только для скопа for-цикла.

Конечно, в данном дизайне сохраняется изначальный подход к ошибкам как к обычным значениям. Вы всё также вольны использовать обычный if для проверки ошибки, а в обработчике ошибок (handle) можно (и нужно) делать то, что наилучшим образом подходит ситуации – например, дополнять ошибку деталями перед тем, как обработать в другом обработчике:

type Error struct {
    Func string
    User string
    Path string
    Err  error
}

func (e *Error) Error() string

func ProcessFiles(user string, files chan string) error {
    e := Error{ Func: "ProcessFile", User: user}
    handle err { e.Err = err; return &e } // handler A
    u := check OpenUserInfo(user)         // check 1
    defer u.Close()
    for file := range files {
        handle err { e.Path = file }       // handler B
        check process(check os.Open(file)) // check 2
    }
    ...
}

Стоит отметить, что handle несколько напоминает defer, и можно решить, что порядок вызова будет аналогичным, но это не так. Эта разница – одна из слабых место данного дизайна, кстати. Кроме того, handler B будет исполнен только раз – аналогичный вызов defer в том же месте, привёл бы ко множественным вызовам. Go команда пыталась найти способ унифицировать defer/panic и handle/check механизмы, но не нашла разумного варианта, который бы не делал язык обратно-несовместимым.

Ещё важный момент – хотя бы один обработчик должен возвращать значения (т.е. вызывать return), если оригинальная функция что-то возвращает. В противном случае это будет ошибкой компиляции.

Паника (panic) в обработчиках исполняется так же, как и в теле функции.

Обработчик по-умолчанию

Ещё одна ошибка компиляции – если код обработчика пуст (handle err {}). Вместо этого вводится понятие «обработчика по-умолчанию» (default handler). Если не определять никакой handle блок, то, по-умолчанию, будет возвращаться та же самая ошибка, которую получил check и остальные переменные без изменений (в именованных возвратных значениях; в неименованных будут возвращаться нулевые значения — zero values).

Пример кода с обработчиком по-умолчанию:

func printSum(a, b string) error {
    x := check strconv.Atoi(a)
    y := check strconv.Atoi(b)
    fmt.Println("result:", x + y)
    return nil
}

Сохранение стека вызова

Для корректных стектрейсов Go трактует обработчики как код, вызывающийся из функции в своем собственном стеке. Нужен будет какой-то механизм, позволяющий игнорировать код обработчика в стектрейсе, например для табличных тестов. Скорее всего, вот использование t.Helper() будет достаточно, но это ещё открытый вопрос:

func TestFoo(t *testing.T) {
    handle err {
        t.Helper()
        t.Fatal(err)
    }
    for _, tc := range testCases {
        x := check Foo(tc.a)
        y := check Foo(tc.b)
        if x != y {
            t.Errorf("Foo(%v) != Foo(%v)", tc.a, tc.b)
        }
    }
}

Затенение (shadowing) переменных

Использование check может практически убрать надобность в переопределении переменных в краткой форме присваивания (:=), поскольку это было продиктовано именно необходимостью переиспользовать err. С новым механизмом handle/check затенение переменных может вообще стать неактуальным.

Открытые вопросы

defer/panic

Использование похожих концепций (defer/panic и handle/check) увеличивает когнитивную нагрузку на программиста и сложность языка. Не очень очевидные различия между ними открывают двери для нового класса ошибок и неправильного использования обоих механизмов.

Поскольку handle всегда вызывается раньше defer (и, напомню, паника в коде обработчика обрабатывается так же, как и в обычном теле функции), то нет способа использовать handle/check в теле defer-а. Вот этот код не скомпилируется:

func Greet(w io.WriteCloser) error {
    defer func() {
        check w.Close()
    }()
    fmt.Fprintf(w, "hello, worldn")
    return nil
}

Пока не ясно, как можно красиво решить эту ситуацию.

Уменьшение локальности кода

Одним из главных преимуществ нынешнего механизма обработки ошибок в Go является высокая локальность – код обработчика ошибки находится очень близко к коду получения ошибки, и исполняется в том же контексте. Новый же механизм вводит контекстно-зависимый «прыжок», похожий одновременно на исключения, на defer, на break и на goto. И хотя данный подход сильно отличается от исключений, и больше похож на goto, это всё ещё одна концепция, которую программисты должны будут учить и держать в голове.

Имена ключевых слов

Рассматривалось использование таких слов как try, catch, ? и других, потенциально более знакомых из других языков. После экспериментирования со всеми, авторы Go считают, что check и handle лучше всего вписываются в концепцию и уменьшают вероятность неверного трактования.

Что делать с кодом, в котором имена handle и catch уже определены, пока тоже не ясно (не факт, что это будут ключевые слова (keywords) ещё).

Часто задаваемые вопросы

Когда выйдет Go2?

Неизвестно. Учитывая прошлый опыт нововведений в Go, от стадии обсуждения до первого экспериментального использования проходит 2-3 релиза, а официальное введение – ещё через релиз. Если отталкиваться от этого, то это 2-3 года при наилучших раскладах.

Плюс, не факт, что это будет Go2 – это вопрос брендинга. Скорее всего, будет обычный релиз очередной версии Go – Go 1.20 например. Никто не знает.

Разве это не то же самое, что исключения?

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

Не разделит ли это Go программистов на 2 лагеря – тех, кто останется верным if err != nil {} проверкам, и сторонников handle/check?

Неизвестно, но расчёт на то, что if err будет мало смысла использовать, кроме специальных случаев – новый дизайн уменьшает количество символов для набора, и сохраняет явность проверки и обработки ошибок. Но, время покажет.

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

Является. Расчёт на то, что выгода от этого усложнения перевесит минусы самого факта усложнения.

Это окончательный дизайн и точно ли он будет принят?

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

Я знаю, как сделать дизайн лучше! Что мне делать?

Напишите статью с объяснением вашего видения и добавьте её в вики-страничку Go2ErrorHandlingFeedback

Резюме

  • Предложен новый механизм обработки ошибок в будущих версиях Go —  handle/check
  • Обратно-совместим с нынешним
  • Проверка и обработка ошибок остаются явными
  • Сокращается количество текста, особенно в кусках кода, где много повторений однотипных ошибок
  • В грамматику языка добавляются два новых элемента
  • Есть открытые/нерешённые вопросы (взаимодействие с defer/panic)

Ссылки

  • Обзор проблемы обработки ошибок
  • Черновик дизайна нового механизма обработки ошибок
  • Статьи с реакциями на предложенный дизайн (вики)
  • Отчёты об использовании с описанием проблемы обработки ошибок в Go
  • Анонс черновиков дизайна Go 2

Мысли? Комментарии?

  • Image credits: Gopher Artwork by Ashley McNamara

Error handling in Go is a little different than other mainstream programming languages like Java, JavaScript, or Python. Go’s built-in errors don’t contain stack traces, nor do they support conventional try/catch methods to handle them. Instead, errors in Go are just values returned by functions, and they can be treated in much the same way as any other datatype — leading to a surprisingly lightweight and simple design.

In this article, I’ll demonstrate the basics of handling errors in Go, as well as some simple strategies you can follow in your code to ensure your program is robust and easy to debug.

The Error Type

The error type in Go is implemented as the following interface:

type error interface {
    Error() string
}

So basically, an error is anything that implements the Error() method, which returns an error message as a string. It’s that simple!

Constructing Errors

Errors can be constructed on the fly using Go’s built-in errors or fmt packages. For example, the following function uses the errors package to return a new error with a static error message:

package main

import "errors"

func DoSomething() error {
    return errors.New("something didn't work")
}

Similarly, the fmt package can be used to add dynamic data to the error, such as an int, string, or another error. For example:

package main

import "fmt"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("can't divide '%d' by zero", a)
    }
    return a / b, nil
}

Note that fmt.Errorf will prove extremely useful when used to wrap another error with the %w format verb — but I’ll get into more detail on that further down in the article.

There are a few other important things to note in the example above.

  • Errors can be returned as nil, and in fact, it’s the default, or “zero”, value of on error in Go. This is important since checking if err != nil is the idiomatic way to determine if an error was encountered (replacing the try/catch statements you may be familiar with in other programming languages).

  • Errors are typically returned as the last argument in a function. Hence in our example above, we return an int and an error, in that order.

  • When we do return an error, the other arguments returned by the function are typically returned as their default “zero” value. A user of a function may expect that if a non-nil error is returned, then the other arguments returned are not relevant.

  • Lastly, error messages are usually written in lower-case and don’t end in punctuation. Exceptions can be made though, for example when including a proper noun, a function name that begins with a capital letter, etc.

Defining Expected Errors

Another important technique in Go is defining expected Errors so they can be checked for explicitly in other parts of the code. This becomes useful when you need to execute a different branch of code if a certain kind of error is encountered.

Defining Sentinel Errors

Building on the Divide function from earlier, we can improve the error signaling by pre-defining a “Sentinel” error. Calling functions can explicitly check for this error using errors.Is:

package main

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        switch {
        case errors.Is(err, ErrDivideByZero):
            fmt.Println("divide by zero error")
        default:
            fmt.Printf("unexpected division error: %sn", err)
        }
        return
    }

    fmt.Printf("%d / %d = %dn", a, b, result)
}

Defining Custom Error Types

Many error-handling use cases can be covered using the strategy above, however, there can be times when you might want a little more functionality. Perhaps you want an error to carry additional data fields, or maybe the error’s message should populate itself with dynamic values when it’s printed.

You can do that in Go by implementing custom errors type.

Below is a slight rework of the previous example. Notice the new type DivisionError, which implements the Error interface. We can make use of errors.As to check and convert from a standard error to our more specific DivisionError.

package main

import (
    "errors"
    "fmt"
)

type DivisionError struct {
    IntA int
    IntB int
    Msg  string
}

func (e *DivisionError) Error() string { 
    return e.Msg
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivisionError{
            Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
            IntA: a, IntB: b,
        }
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        var divErr *DivisionError
        switch {
        case errors.As(err, &divErr):
            fmt.Printf("%d / %d is not mathematically valid: %sn",
              divErr.IntA, divErr.IntB, divErr.Error())
        default:
            fmt.Printf("unexpected division error: %sn", err)
        }
        return
    }

    fmt.Printf("%d / %d = %dn", a, b, result)
}

Note: when necessary, you can also customize the behavior of the errors.Is and errors.As. See this Go.dev blog for an example.

Another note: errors.Is was added in Go 1.13 and is preferable over checking err == .... More on that below.

Wrapping Errors

In these examples so far, the errors have been created, returned, and handled with a single function call. In other words, the stack of functions involved in “bubbling” up the error is only a single level deep.

Often in real-world programs, there can be many more functions involved — from the function where the error is produced, to where it is eventually handled, and any number of additional functions in-between.

In Go 1.13, several new error APIs were introduced, including errors.Wrap and errors.Unwrap, which are useful in applying additional context to an error as it “bubbles up”, as well as checking for particular error types, regardless of how many times the error has been wrapped.

A bit of history: Before Go 1.13 was released in 2019, the standard library didn’t contain many APIs for working with errors — it was basically just errors.New and fmt.Errorf. As such, you may encounter legacy Go programs in the wild that do not implement some of the newer error APIs. Many legacy programs also used 3rd-party error libraries such as pkg/errors. Eventually, a formal proposal was documented in 2018, which suggested many of the features we see today in Go 1.13+.

The Old Way (Before Go 1.13)

It’s easy to see just how useful the new error APIs are in Go 1.13+ by looking at some examples where the old API was limiting.

Let’s consider a simple program that manages a database of users. In this program, we’ll have a few functions involved in the lifecycle of a database error.

For simplicity’s sake, let’s replace what would be a real database with an entirely “fake” database that we import from "example.com/fake/users/db".

Let’s also assume that this fake database already contains some functions for finding and updating user records. And that the user records are defined to be a struct that looks something like:

package db

type User struct {
  ID       string
  Username string
  Age      int
}

func FindUser(username string) (*User, error) { /* ... */ }
func SetUserAge(user *User, age int) error { /* ... */ }

Here’s our example program:

package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    return db.Find(username)
}

func SetUserAge(u *db.User, age int) error {
    return db.SetAge(u, age)
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return err
  }

  if err = SetUserAge(user, age); err != nil {
      return err
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

Now, what happens if one of our database operations fails with some malformed request error?

The error check in the main function should catch that and print something like this:

failed finding or updating user: malformed request

But which of the two database operations produced the error? Unfortunately, we don’t have enough information in our error log to know if it came from FindUser or SetUserAge.

Go 1.13 adds a simple way to add that information.

Errors Are Better Wrapped

The snippet below is refactored so that is uses fmt.Errorf with a %w verb to “wrap” errors as they “bubble up” through the other function calls. This adds the context needed so that it’s possible to deduce which of those database operations failed in the previous example.

package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    u, err := db.Find(username)
    if err != nil {
        return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
    }
    return u, nil
}

func SetUserAge(u *db.User, age int) error {
    if err := db.SetAge(u, age); err != nil {
      return fmt.Errorf("SetUserAge: failed executing db update: %w", err)
    }
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  if err = SetUserAge(user, age); err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

If we re-run the program and encounter the same error, the log should print the following:

failed finding or updating user: FindAndSetUserAge: SetUserAge: failed executing db update: malformed request

Now our message contains enough information that we can see the problem originated in the db.SetUserAge function. Phew! That definitely saved us some time debugging!

If used correctly, error wrapping can provide additional context about the lineage of an error, in ways similar to a traditional stack-trace.

Wrapping also preserves the original error, which means errors.Is and errors.As continue to work, regardless of how many times an error has been wrapped. We can also call errors.Unwrap to return the previous error in the chain.

When To Wrap

Generally, it’s a good idea to wrap an error with at least the function’s name, every time you “bubble it up” — i.e. every time you receive the error from a function and want to continue returning it back up the function chain.

Wrapping an error adds the gift of context

There are some exceptions to the rule, however, where wrapping an error may not be appropriate.

Since wrapping the error always preserves the original error messages, sometimes exposing those underlying issues might be a security, privacy, or even UX concern. In such situations, it could be worth handling the error and returning a new one, rather than wrapping it. This could be the case if you’re writing an open-source library or a REST API where you don’t want the underlying error message to be returned to the 3rd-party user.

While you’re here:

Earthly is the effortless CI/CD framework.
Develop CI/CD pipelines locally and run them anywhere!

Conclusion

That’s a wrap! In summary, here’s the gist of what was covered here:

  • Errors in Go are just lightweight pieces of data that implement the Error interface
  • Predefined errors will improve signaling, allowing us to check which error occurred
  • Wrap errors to add enough context to trace through function calls (similar to a stack trace)

I hope you found this guide to effective error handling useful. If you’d like to learn more, I’ve attached some related articles I found interesting during my own journey to robust error handling in Go.

References

  • Error handling and Go
  • Go 1.13 Errors
  • Go Error Doc
  • Go By Example: Errors
  • Go By Example: Panic

Get notified about new articles!

We won’t send you spam. Unsubscribe at any time.

In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.

Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.

In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.

Errors in Golang

Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using an error.

package main

import (  
    "fmt"
    "ioutil"
)

func main() {  
    dir, err := ioutil.TempDir("", "temp")
		if err != nil {
			return fmt.Errorf("failed to create temp dir: %v", err)
		}
}

Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.

Creating custom errors

As already mentioned errors are represented using the built-in error interface type, which has the following definition:

type error interface {  
    Error() string
}

The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.

There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.

String-based errors

String-based errors can be created using two out-of-the-box options in Golang and are used for simple errors that just need to return an error message.

err := errors.New("math: divided by zero")

The errors.New() method can be used to create new errors and takes the error message as its only parameter.

err2 := fmt.Errorf("math: %g cannot be divided by zero", x)

fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.

Custom error with data

You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:

type PathError struct {
    Path string
}

func (e *PathError) Error() string {
	return fmt.Sprintf("error in path: %v", e.Path)
}

The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.

Here is an elementary example:

package main

import(
	"fmt"
)

type PathError struct {
    Path string
}

func (e *PathError) Error() string {
	return fmt.Sprintf("error in path: %v", e.Path)
}

func throwError() error {
	return &PathError{Path: "/test"}
}

func main() {
	err := throwError()

	if err != nil {
		fmt.Println(err)
	}
}

You can also check if the error has a specific type using either an if or switch statement:

if err != nil {
    switch e := err.(type) {
    case *PathError :
        // Do something with the path
    default:
        log.Println(e)
    }
}

This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.

e.GetInfo()

Error handling in functions

Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.

Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function as follows:

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0.0, errors.New("cannot divide through zero")
	}

	return a/b, nil
}

The function call will then look similar to this:

func main() {
	num, err := divide(100, 0)

	if err != nil {
		fmt.Printf("error: %s", err.Error())
	} else {
		fmt.Println("Number: ", num)
	}
}

If the returned error is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.

Take the following code for example:

package main

import (
	"errors"
	"fmt"
)

func main() {
	num2, _ := divide(100, 0)
	
	fmt.Println("Number: ", num2)
}

The so-called blank identifier is used as an anonymous placeholder and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.

Defer, panic and recover

Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.

Defer

A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.

Take the following function for example:

func processHTML(url string) error {
  resp, err := http.Get(url)

  if err != nil {
    	return err
	}

	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
		resp.Body.Close()
		return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
	}

	doc, err := html.Parse(resp.Body)
	resp.Body.Close()

	// ... Process HTML ...
	
	return nil
}

Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.

Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.

func processHTMLDefer(url string) error {
  resp, err := http.Get(url)

  if err != nil {
    	return err
	}
  defer resp.Body.Close()

	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
		return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
	}

	doc, err := html.Parse(resp.Body)

	// ... Process HTML ...

	return nil
}

All deferred functions are executed in reverse order in which they were deferred when the function finishes.

package main

import (
        "fmt"
)

func main() {
	first()
}

func first() {
	defer fmt.Println("first")
	second()
}

func second() {
	defer fmt.Println("second")
	third()
}

func third() {
	defer fmt.Println("third")
}

Here is the result of running the above program:

third
second
first

Panic

A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.

As an example Golang will panic when a number is divided by zero.

package main

import "fmt"

func main() {
	divide(5)
}

func divide(x int) {
	fmt.Printf("divide(%d) n", x+0/x)
	divide(x-1)
}

Once the divide function is called using zero, the program will panic, resulting in the following output.

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.divide(0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2

You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.

func getArguments() {
	if len(os.Args) == 1 {
		panic("Not enough arguments!")
	}
}

As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.

package main

import (
	"fmt"
)

func main() {
	accessSlice([]int{1,2,5,6,7,8}, 0)
}

func accessSlice(slice []int, index int) {
	fmt.Printf("item %d, value %d n", index, slice[index])
	defer fmt.Printf("defer %d n", index)
	accessSlice(slice, index+1)
}

Here is the output of the programm:

item 0, value 1 
item 1, value 2 
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
panic: runtime error: index out of range [6] with length 6

goroutine 1 [running]:
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x6)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:29 +0x250
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:9 +0x99
exit status 2

Recover

In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.

Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.

package main

import "fmt"

func main(){
	accessSlice([]int{1,2,5,6,7,8}, 0)
}

func accessSlice(slice []int, index int) {
	defer func() {
		if p := recover(); p != nil {
			fmt.Printf("internal error: %v", p)
		}
	}()

	fmt.Printf("item %d, value %d n", index, slice[index])
	defer fmt.Printf("defer %d n", index)
	accessSlice(slice, index+1)
}

As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.

Output:

item 0, value 1 
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6defer 5 
defer 4
defer 3
defer 2
defer 1
defer 0

Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.

Error wrapping

Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.

You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown in the following example.

package main

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

func main() {
	err := openFile("non-existing")

	if err != nil {
		fmt.Printf("error running program: %s n", err.Error())
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}

	return nil
}

The output of the application would now look like the following:

error running program: error opening non-existing: open non-existing: no such file or directory

As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back by unwrapping the error using errors.Unwrap.

package main

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

func main() {
	err := openFile("non-existing")

	if err != nil {
		fmt.Printf("error running program: %s n", err.Error())

		// Unwrap error
		unwrappedErr := errors.Unwrap(err)
		fmt.Printf("unwrapped error: %v n", unwrappedErr)
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}

	return nil
}

As you can see the output now also displays the original error.

error running program: error opening non-existing: open non-existing: no such file or directory 
unwrapped error: open non-existing: no such file or directory

Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.

Casting Errors

Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.

Let’s look at the official errors.As docs example to better understand what is happening.

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main(){
	// Casting error
	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)
		}
	}
}

Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.

Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return false.

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main(){
	// Check if error is a specific type
	if _, err := os.Open("non-existing"); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			fmt.Println("file does not exist")
		} else {
			fmt.Println(err)
		}
	}
}

After checking, you can adapt your error message accordingly.

Sources

  • Golang Blog — Working with Errors in Go 1.13
  • The Go Programming language book
  • Golang Blog — Defer, Panic, and Recover
  • LogRocket — Error handling in Golang
  • GolangByExample — Wrapping and Un-wrapping of error in Go
  • Golang Documentation — Package errors

Conclusion

You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.

If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.

Quick Summary:

Golang Error Handling has been the talk of the town because of its unconventional approach, unlike other languages that utilize try…catch block. It was quite difficult for the developers to digest the new process of Golang error handling patterns.

Go’s way of error handling has also been questioned and criticized as it was entirely out of the box. But, after few months of frustration, the technique of Golang error best practices proved to be remarkable. In this blog, I’ll discuss the basics of Golang Error Handling with examples and why is it having a better approach than other languages. For simplifying the blog, I’ve classified it into sections.

Before moving on to Golang Error Handling’s technique, I would like to discuss a bit about Error and Error handling.

Errors are defined as the unwanted and unusual conditions encountered in the program. It can be either compile time or run time. Various examples are – accessing a file that does not exist, a failed db connection, or abnormal user inputs. Anything could generate an error.

Now the process for predicting where your program could behave abnormally and the technique of implementing the solution for further diagnosis is Error Handling. You might be familiar try…catch block for handling errors in Java, PHP, or Python.

Now let’s start with Error handling in Golang.

Exploring Golang Error Handling

Getting familiar with new approaches has always been difficult, no matter how clean and straightforward it can be. And, when people get frustrated with such new methods, they start criticizing them. That’s what happened with Go. Developers were dealing with conventional techniques; thus, it was quite challenging for them to make room for Go’s way of error handling. Many proposals were made to change and improve Golang Error Handling patterns, as you can see in this image taken from github.

There’s a lot to learn about the methods of Go error handling but before that, I would like to discuss the built-in error type of Golang.

The Error Type

If you’ve ever coded in Go you would be quite familiar with the error type. Now, the question might arise what is this error type?

The error type is nothing but a type of an interface. It is the type given to the error variable which declares itself as a string.

The syntax looks something like this-


Copy Text

type error interface {
    Error() string
}

Exploring Golang Error Handling Patterns

Golang’s philosophy behind error handling is very straightforward – Don’t overlook errors; they are critically important. The syntax of func f() (value, error) is quite a piece of cake to learn and implement, even for those who have just started with Go.

Golang Error Handling techniques implicitly force the developers to use errors as first-class values of the functions. In case you have missed returning the error string from your function like this –


Copy Text

func getUsers() (*Users, error) { .... }

func main() {
    users, _ := getUsers()
}

Almost all the IDEs and linters will notice that you’ve missed returning the error and will make it salient for your fellow developers while reviewing the code. In this way, Golang doesn’t force you to use error as a first-class value of your function ‘explicitly,’ but neither does it allow you to overlook it.

Golang just provides a built-in error type due to which you don’t forget how critical these errors can be. If you choose not to fire any actions when the program encounters an error because of err != nil, you have to be prepared for the consequences; even Golang would be unable to save you! Let’s have one example of Golang error handling best practices.


Copy Text

if error := criticalOperation(); error != nil {
    // Not returning anything is a bad practice.   
log.Printf("Oops! Something went wrong in the program", error)
    // `return` your error message hereafter this line!
}

if error := saveData(data); error != nil {
    return fmt.Errorf("Data has been lost", error)
}

When err != nil is encountered while calling criticalOperation() and if you choose to log the error message instead of handling it intelligently, even Go won’t save your program from the errors. Golang just provides you how to return and use the errors; further, Error handling in golang handling the Go errors is entirely up to you.

Golang prefers to use the panic and recover method rather than throwing exceptions and using try…catch block. We will learn more about that later. I hope you now had a basic idea of Go error handling. Now, let’s see why the Error handling in golang is better than other languages. And for that, we need to learn a bit about how different languages handle their errors.

Do you need assistance to solve your Golang error?
Hire Golang developer from us to fix the bugs and fine-tune your Golang app user experience.

Throwing Exception: Error Handling Way of Other Programming Languages

Those developers familiar with Javascript frameworks, Java, Python, Ruby, and PHP, might better understand how these languages handle their errors. Look at this code snippet of how to throw an exception-


Copy Text

try {
    criticalDataOperation1();
    criticalDataOperation2();
    criticalDataOperation3();
} catch (err) {
    console.error(err);
}

While executing the function criticalDataOperations(), it will jump to the catch block if an error occurs, and console.log(err) will be performed. The function criticalOperations() doesn’t have to explicitly state the flow of error, for which it will jump the catch block. If any exception is thrown while executing these functions, then the program will directly log the error. And this is the advantage of exception-based programs: if you have forgotten to handle some exceptions, then also the stack trace will notice it at the run time and move forward to catch block.

Throwing exceptions is not the only way of error handling; Rust is also one of its types. Rust provides good pattern matching with simple syntax to search errors and acquire similar results like exceptions.

Isn’t it strange to digest why Golang didn’t utilize exceptions, a conventional way of error handling, and came up with such a unique approach? Let’s quench our curiosity and dive for the answer.

Why didn’t Golang utilize exceptions, a conventional way to handle errors?

Two key points that are kept in mind while Golang error handling is:

  • Keep it simple.
  • Plan where it can go wrong.

Golang tends to keep the syntax and usage of if err != nil as simple as possible. It returns (value, err) from the function to ensure how to handle the program’s failure. You don’t need to stress yourself with the complications of nested try…catch blocks. The practice of exception-based code never lets the developers search the actual errors; instead, they will throw the exception, which will be handled in the catch block.

Developers are forced to analyze every situation in exception-based languages and throw exceptions without adequately addressing them. Whereas, Golang return error handle your errors and return them as values from the functions.

Here are the advantages of Golang new error handling.

  • Transparent control-flow.
  • No interruption of sudden uncaught exceptions.
  • You have full control over the errors as they are considered as values – you can do whatever with that.
  • Simple syntax.
  • Easy implementation of error chains to take action on the error.

The last point might seem quite confusing to you. Let me make it simpler for you. The easy syntax of if err != nil allows you to chain the functions returning errors throughout the hierarchy of your program until you have reached the actual error, which has to be handled precisely. The practice of chaining the errors can be relatively easy to traverse and debug, even for your teammates.

Here is the example for error-chaining.


Copy Text

// controllers/users.go
if error := db.CreateUserforDB(user); error != nil {
    return fmt.Errorf("error while creating user: %w", error)
}

// database/users.go
func (db *Database) CreateUserforDB(user *User) error {
    ok, error := db.DoesUserExistinDB(user)
    if error != nil {
        return fmt.Errorf("error in db while checking: %w", err)
    }
    ...
}

func (db *Database) DoesUserExistinDB(user *User) error {
    if error := db.Connected(); error != nil {
        return fmt.Errorf("error while establishing connection: %w", err)
    }
    ...
}

func (db *Database) Connected() error {
    if !isInternetConnectionEstablished() {
        return errors.New("not connected to internet")
    }
    ...
}

The advantage of the above code is that every block has returned informative errors that can be easily understood and are responsible for those errors they are aware of. This kind of Golang handle error chaining helps your program not to break unexpectedly and makes traversing of errors less time taking. You can also choose to use the stack trace in your function and utilize this library for exploring various built-in functions.

So far, we have seen Golang Error Handling best practices and fundamental way of using if…err != nil. Do you remember I have used panic and recover before, let’s see what the fuss is about?

Golang Error Handling: Panic and Recover Mechanism

As I have mentioned before, Golang has panic and recover rather than try and catch blocks. You might have seen try…catch block so many times in the program, so I believe the exception handling is not so exceptionally handled – what an irony! Sometimes, developers use exception handling to throw a custom error message; this usage complicates runtime errors and custom errors (avoid such practices).

Whereas Golang has a different method for custom errors, we have learned so far, i.e., of throwing Golang a custom error message by returning the error as the function’s value. And panic and recover technique is used in exceptional cases only, unlike try and catch.

If there’s a truly exceptional case for which you have to use a panic scenario; it will stop the regular function’s flow and start panicking. When function func has called panic(), the func won’t be executed further though other deferred functions will be performed as expected.

Recover is the built-in function that frees the function from its panicking state. It is only used inside the deferred functions. While executing the function normally, recover will return nil without any other effects.

Here is a simple code snippet for better understanding.

Panicking


Copy Text

exceptionalCondition := true
if exceptionalCondition {
    panic("panicking!!")
}

Creating panic in the programs is more manageable than handling it.

Recover: To Rescue From Panic


Copy Text

func F() {
   defer func() {
     if error := recover(); error != nil {
	  fmt.Println("This is the error: ", err)
	}()
  //do whatever here...
}

You can add an anonymous function or make a custom function using defer keyword.

This was a high overview of what is panic and recover mechanism and how does it work.

Conclusion

Thus, this was all about Golang Error Handling basics, how it is better than languages, and a high overview of panic and recover mechanism. I hope that this blog has helped you the way you have expected it. Being a globally renowned Golang development company, we have established our reputation in providing best-in-class services to cater to your golang project requirements. Hire Golang developer from us and turn your idea into a reality that is suited to your business needs.

Александр Тихоненко

Александр Тихоненко


Ведущий разработчик трайба «Автоматизация бизнес-процессов» МТС Диджитал

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

Выглядит это так:

type error interface {  
    Error() string
}

В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.

  1. Как обрабатывать ошибки в Go?
  2. Создание ошибок
  3. Оборачивание ошибок
  4. Проверка типов с Is и As
  5. Сторонние пакеты по работе с ошибками в Go
  6. Defer, panic and recover
  7. После изложенного

Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error и проверить её на nil:

if err != nil {
	return err
}

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

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

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

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

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

  • errors.New
  • fmt.Errorf

Метод errors.New() создаёт ошибку, принимая в качестве параметра текстовое сообщение.

package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.New("emit macho dwarf: elf header corrupted")
	fmt.Print(err)
}

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

package main

import (
	"fmt"
)

func main() {
	const name, id = "bueller", 17
	err := fmt.Errorf("user %q (id %d) not found", name, id)
	fmt.Print(err)
}

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

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

Поскольку Error — это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.

package main

import (
  "fmt"
)

type NotFoundError struct {
  UserId int
}

func (err NotFoundError) Error() string {
  return fmt.Sprintf("user with id %d not found", err.UserId)
}

func SearchUser(id int) error {
  // some logic for search
  // ...
  // if not found
  var err NotFoundError
  err.UserId = id
  return err
}

func main() {
  const id = 17
  err := SearchUser(id)
  if err != nil {
     fmt.Println(err)
     //type error checking
     notFoundErr, ok := err.(NotFoundError)
     if ok {
        fmt.Println(notFoundErr.UserId)
     }
  }
}

Представим другую ситуацию. У нас есть метод, который вызывает внутри себя ещё один метод. В каждом из них проверяется своя ошибка. Иногда требуется в метод верхнего уровня передать сразу обе эти ошибки.

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

Также для оборачивания ошибок в fmt.Errorf есть плейсхолдер %w, который и позволяет произвести такую упаковку.:

package main

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

func main() {
	err := openFile("non-existing")
	if err != nil {
		fmt.Println(err.Error())
		// get internal error
		fmt.Println(errors.Unwrap(err))
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}
	return nil
}

Проверка типов с Is и As

В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:

  • errors.Is
  • errors.As

Метод errors.Is, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main() {
	if _, err := os.Open("non-existing"); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			fmt.Println("file does not exist")
		} else {
			fmt.Println(err)
		}
	}
}

Если это будет та же самая ошибка, то функция вернёт true, если нет — false.

errors.As проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):

package main

	import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

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

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

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

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

В качестве примера можно посмотреть на пакет pkg/errors. Одной из его способностей является логирование stack trace:

package main

import (
	"fmt"
	"github.com/pkg/errors"
)

func main() {
	err := errors.Errorf("whoops: %s", "foo")
	fmt.Printf("%+v", err)
}
	// Example output:
	// whoops: foo
	// github.com/pkg/errors_test.ExampleErrorf
	//         /home/dfc/src/github.com/pkg/errors/example_test.go:101
	// testing.runExample
	//         /home/dfc/go/src/testing/example.go:114
	// testing.RunExamples
	//         /home/dfc/go/src/testing/example.go:38
	// testing.(*M).Run
	//         /home/dfc/go/src/testing/testing.go:744
	// main.main
	//         /github.com/pkg/errors/_test/_testmain.go:102
	// runtime.main
	//         /home/dfc/go/src/runtime/proc.go:183
	// runtime.goexit
	//         /home/dfc/go/src/runtime/asm_amd64.s:2059

Defer, panic and recover

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

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

Для работы с такими ошибками существует механизм «defer, panic, recover»

Defer

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

package main

import (
    "fmt"
    "os"
)

func main() {
    f := createFile("/tmp/defer.txt")
    defer closeFile(f)
    writeFile(f)
}

func createFile(p string) *os.File {
    fmt.Println("creating")
    f, err := os.Create(p)
    if err != nil {
        panic(err)
    }
    return f
}

func writeFile(f *os.File) {
    fmt.Println("writing")
    fmt.Fprintln(f, "data")
}

func closeFile(f *os.File) {
    fmt.Println("closing")
    err := f.Close()
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %vn", err)
        os.Exit(1)
    }
}

Panic

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

Например, Golang будет «паниковать», когда число делится на ноль:

panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2

Также панику можно вызвать явно с помощью метода panic(). Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.

Recover

Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.

Recover всегда должна вызываться в функции defer. ​​Чтобы сообщить об ошибке как возвращаемом значении, вы должны вызвать функцию recover в той же горутине, что и паника, получить структуру ошибки из функции восстановления и передать её в переменную:

package main

import (
	"errors"
	"fmt"
)

func A() {
	defer fmt.Println("Then we can't save the earth!")
	defer func() {
		if x := recover(); x != nil {
			fmt.Printf("Panic: %+vn", x)
		}
	}()
	B()
}

func B() {
	defer fmt.Println("And if it keeps getting hotter...")
	C()
}

func C() {
	defer fmt.Println("Turn on the air conditioner...")
	Break()
}

func Break() {
	defer fmt.Println("If it's more than 30 degrees...")
	panic(errors.New("Global Warming!!!"))
}

func main() {
	A()
}

После изложенного

Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.

Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.

Errors are a language-agnostic part that helps to write code in such a way that no unexpected thing happens. When something occurs which is not supported by any means then an error occurs. Errors help to write clean code that increases the maintainability of the program.

What is an error?

An error is a well developed abstract concept which occurs when an exception happens. That is whenever something unexpected happens an error is thrown. Errors are common in every language which basically means it is a concept in the realm of programming.

Why do we need Error?

Errors are a part of any program. An error tells if something unexpected happens. Errors also help maintain code stability and maintainability. Without errors, the programs we use today will be extremely buggy due to a lack of testing.

Golang has support for errors in a really simple way. Go functions returns errors as a second return value. That is the standard way of implementing and using errors in Go. That means the error can be checked immediately before proceeding to the next steps.

Simple Error Methods

There are multiple methods for creating errors. Here we will discuss the simple ones that can be created without much effort.

1. Using the New function

Golang errors package has a function called New() which can be used to create errors easily. Below it is in action.

package main

import (
	"fmt"
	"errors"
)

func e(v int) (int, error) {
	if v == 0 {
		return 0, errors.New("Zero cannot be used")
	} else {
		return 2*v, nil
	}
}

func main() {
	v, err := e(0)
	
	if err != nil {
		fmt.Println(err, v)      // Zero cannot be used 0
	}	
}

2. Using the Errorf function

The fmt package has an Errorf() method that allows formatted errors as shown below.

fmt.Errorf("Error: Zero not allowed! %v", v)    // Error: Zero not allowed! 0

Checking for an Error

To check for an error we simply get the second value of the function and then check the value with the nil. Since the zero value of an error is nil. So, we check if an error is a nil. If it is then no error has occurred and all other cases the error has occurred.

package main

import (
	"fmt"
	"errors"
)

func e(v int) (int, error) {
	return 42, errors.New("42 is unexpected!")
}

func main() {
	_, err := e(0)
	
	if err != nil {   // check error here
		fmt.Println(err)      // 42 is unexpected!
	}	
}

Panic and recover

Panic occurs when an unexpected wrong thing happens. It stops the function execution. Recover is the opposite of it. It allows us to recover the execution from stopping. Below shown code illustrates the concept.

package main

import (
	"fmt"
)

func f(s string) {
	panic(s)      // throws panic
}

func main() {
        // defer makes the function run at the end
	defer func() {      // recovers panic
		if e := recover(); e != nil {
            		fmt.Println("Recovered from panic")
        	}
	}()
	
	f("Panic occurs!!!") // throws panic 
	
	// output:
	// Recovered from panic
}

Creating custom errors

As we have seen earlier the function errors.New() and fmt.Errorf() both can be used to create new errors. But there is another way we can do that. And that is implementing the error interface.

type CustomError struct {
	data string
}

func (e *CustomError) Error() string {
	return fmt.Sprintf("Error occured due to... %s", e.data)
}

Returning error alongside values

Returning errors are pretty easy in Go. Go supports multiple return values. So we can return any value and error both at the same time and then check the error. Here is a way to do that.

import (
	"fmt"
	"errors"
)

func returnError() (int, error) {  // declare return type here
	return 42, errors.New("Error occured!")  // return it here
}

func main() {
	v, e := returnError()
	if e != nil {
		fmt.Println(e, v)  // Error occured! 42
	}
}

Ignoring errors in Golang

Go has the skip (-) operator which allows skipping returned errors at all. Simply using the skip operator helps here.

package main

import (
	"fmt"
	"errors"
)

func returnError() (int, error) {  // declare return type here
	return 42, errors.New("Error occured!")  // return it here
}

func main() {
	v, _ := returnError()   // skip error with skip operator
	
	fmt.Println(v)    // 42
}

PRAVEEN YADAV

Alt Text
Recently, I was working on a project which required to handle custom errors in golang. unlike other language, golang does explicit error checking.

An error is just a value that a function can return if something unexpected happened.

In golang, errors are values which return errors as normal return value then we handle errors like if err != nil compare to other conventional try/catch method in another language.

Error in Golang

type error interface {
    Error() string
}

Enter fullscreen mode

Exit fullscreen mode

In Go, The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.

It has single Error() method which returns the error message as a string. By implementing this method, we can transform any type we define into an error of our own.

package main

import (
    "io/ioutil"
    "log"
)

func getFileContent(filename string) ([]byte, error) {
    content, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return content, nil
}

func main() {
    content, err := getFileContent("filename.txt")
    if err != nil {
        log.SetFlags(0)
        log.Fatal(err)
    }
    log.Println(string(content))
}

Enter fullscreen mode

Exit fullscreen mode

An idiomatic way to handle errors in Go is to return it as the last return value of the function and check for the nil condition.

Creating errors in go

error is an interface type

Golang provides two ways to create errors in standard library using errors.New and fmt.ErrorF.

errors.New

errors.New("new error")

Enter fullscreen mode

Exit fullscreen mode

Go provides the built-in errors package which exports the New function. This function expects an error text message and returns an error.

The returned error can be treated as a string by either accessing err.Error(), or using any of the fmt package functions. Error() method is automatically called by Golang when printing the error using methods like fmt.Println.

var (
    ErrNotFound1 = errors.New("not found")
    ErrNotFound2 = errors.New("not found")
)

func main() {
    fmt.Printf("is identical? : %t", ErrNotFound1 == ErrNotFound2)
}

// Output:
// is identical? : false

Enter fullscreen mode

Exit fullscreen mode

Each call to New, returns a distinct error value even if the text is identical.

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

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

Enter fullscreen mode

Exit fullscreen mode

Internally, errors.New creates and returns a pointer to errors.errorString struct invoked with the string passed which implements the error interface.

fmt.ErrorF

number := 100
zero := 0
fmt.Errorf("math: %d cannot be divided by %d", number, zero)

Enter fullscreen mode

Exit fullscreen mode

fmt.Errorf provides ability to format your error message using format specifier and returns the string as a
value that satisfies error interface.

Custom errors with data in go

As mentioned above, error is an interface type.

Hence, you can create your own error type by implementing the Error() function defined in the error interface on your struct.

So, let’s create our first custom error by implementing error interface.

package main

import (
    "fmt"
    "os"
)

type MyError struct {
    Code int
    Msg  string
}

func (m *MyError) Error() string {
    return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}

func sayHello(name string) (string, error) {
    if name == "" {
        return "", &MyError{Code: 2002, Msg: "no name passed"}
    }
    return fmt.Sprintf("Hello, %s", name), nil
}

func main() {
    s, err := sayHello("")
    if err != nil {
        log.SetFlags(0)
        log.Fatal("unexpected error is ", err)
    }
    fmt.Println(s)
}

Enter fullscreen mode

Exit fullscreen mode

You’ll see the following output:

unexpected error is no name passed: 2002
exit status 1

Enter fullscreen mode

Exit fullscreen mode

In above example, you are creating a custom error using a struct type MyError by implementing Error() function of error interface.

Error wrapping

Golang also allows errors to wrap other errors which is useful when you want to provide additional context to your error messages like providing specific information or more details about the error location in your code.

You can create wrapped errors either with fmt.Errorf or by implementing a custom type. A simple way to create wrapped errors is to call fmt.Errorf with our error which we want to wrap using the %w verb

package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func getFileContent(filename string) ([]byte, error) {
    content, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("error reading file %s: %w", filename, err)
    }
    return content, nil
}

func main() {
    content, err := getFileContent("filename.txt")
    if err != nil {
        log.SetFlags(0)
        log.Fatal(err)
    }
    log.Println(string(content))
}

Enter fullscreen mode

Exit fullscreen mode

You’ll see the following output:

error reading file filename.txt: open filename.txt: no such file or directory
exit status 1

Enter fullscreen mode

Exit fullscreen mode

Examining errors with Is and As

errors.Is

Unwrap, Is and As functions work on errors that may wrap other errors. An error wraps another error if its type has the method Unwrap() which returns a non-nil error.

errors.Is unwraps its first argument sequentially looking for an error that matches the second and returns boolean true if it finds one.
simple equality checks:

if errors.Is(err, ErrNoNamePassed)

Enter fullscreen mode

Exit fullscreen mode

is preferable to

if err == ErrNoNamePassed

Enter fullscreen mode

Exit fullscreen mode

because the former will succeed if err wraps ErrNoNamePassed.

package main

import (
    "errors"
    "fmt"
    "log"
)

type MyError struct {
    Code int
    Msg  string
}

func (m *MyError) Error() string {
    return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}

func main() {
    e1 := &MyError{Code: 501, Msg: "new error"}

    // wrapping e1 with e2
    e2 := fmt.Errorf("E2: %w", e1)
    // wrapping e2 with e3
    e3 := fmt.Errorf("E3: %w", e2)

    fmt.Println(e1) // prints "new error: 501"
    fmt.Println(e2) // prints "E2: new error: 501"
    fmt.Println(e3) // prints "E3: E2: new error: 501"


    fmt.Println(errors.Unwrap(e1)) // prints <nil>
    fmt.Println(errors.Unwrap(e2)) // prints "new error: 501"
    fmt.Println(errors.Unwrap(e3)) // prints E2: new error: 501

    // errors.Is function compares an error to a value.
    if errors.Is(e3, e1) {
        log.SetFlags(0)
        log.Fatal(e3)
    }
}

Enter fullscreen mode

Exit fullscreen mode

We’ll see the following output:

new error: 501
E2: new error: 501
E3: E2: new error: 501
<nil>
new error: 501
E2: new error: 501
E3: E2: new error: 501
exit status 1

Enter fullscreen mode

Exit fullscreen mode

errors.As

errors.As unwraps its first argument sequentially looking for an error that can be assigned to its second argument, which must be a pointer. If it succeeds, it performs the assignment and returns true. Otherwise, it returns false.

var e *MyError
if errors.As(err, &e) {
    fmt.Println(e.code)
}

Enter fullscreen mode

Exit fullscreen mode

is preferable to

if e, ok := err.(*MyError); ok {
    fmt.Println(e)
}

Enter fullscreen mode

Exit fullscreen mode

because the former will succeed if err wraps an *MyError

package main

import (
    "errors"
    "fmt"
    "log"
)

type MyError struct {
    Code int
    Msg  string
}

func (m *MyError) Error() string {
    return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}

func main() {
    e1 := &MyError{Code: 501, Msg: "new error"}
    e2 := fmt.Errorf("E2: %w", e1)

    // errors.As function tests whether an error is a specific type.
    var e *MyError
    if errors.As(e2, &e) {
        log.SetFlags(0)
        log.Fatal(e2)
    }
}

Enter fullscreen mode

Exit fullscreen mode

We’ll see the following output:

E2: new error: 501
exit status 1

Enter fullscreen mode

Exit fullscreen mode

Reference

  1. Standard library error package https://golang.org/pkg/errors/
  2. The Go Blog — Working with Errors in Go 1.13
  3. Error handling in Golang

Conclusion

I hope this article will help you to understand the basics of error handling in Go.

If you have found this useful, please consider recommending and sharing it with other fellow developers and if you have any questions or suggestions, feel free to add a comment or contact me on twitter.

Понравилась статья? Поделить с друзьями:
  • Glow storm gta 5 как исправить
  • Glow device general error
  • Globo gl100 ash ошибка
  • Globe комедия ошибок фильм 2015
  • Global initialization failed как исправить mt4