Golang return error from function

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 >

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 shall be discussing how to return and handle errors effectively using custom and inbuilt Golang functions, with help of practical examples.

Golang return error

An error is basically a fault that occurs in a program execution flow. These errors can be of various natures:- Caused by programmers through code syntax and interface errors , system-related Resources and Runtime errors, algorithm-related logic and arithmetic errors. Which later are solved through debugging process.

In Golang ,The Error is an interface that holds Error() string method. Its implemented as follows

type error interface {
Error() string
}

In an nutshell, when the Error() method is called, it’s return value is in form of string datatype. Through the use of inbuilt Go functions of the fmt and errors packages, we can construct the kind of error message to be displayed. Below is an example to construct Errors using fmt.Error() in Golang, i.e you want to read a file from a given path, unfortunate the file doesn’t exist or the path given is invalid. For example:=

package main

import (
	"fmt"
	"os"
)

func ReadFile(file string) error {
	dataFile, err := os.ReadFile(file)
	if err != nil {
		return fmt.Errorf("An error occurred while Reading the file: open : %v", err)
	}
	fmt.Println(string(dataFile))
	return nil
}
func main() {
	resultsErr := ReadFile("")
	if resultsErr != nil {
		fmt.Printf("%v", resultsErr)
	}
}

Output:

ALSO READ: Golang check if key exists in map [SOLVED]

With the file attached ensure you replace the ReadFile(«test.txt»)

$ go run main.go
Hello

without file attached

$ go run main.go
An error occurred while Reading the file: open: no such file or directory

Explanation:- In the above code, ReadFile() error{} function returns an error which is nil whenever no error encountered. In Golang, the Error return value is nil as the default, or “zero”. Notice that checking if err != nil{} is the idiomatic way to determine if an error was encountered in Golang syntax, in this function we are returning the error only, handling the file data within the function. fmt.Error() enables us to customize the kind of message to be displayed. These messages are always in a lowercase format and don’t end with punctuation.

In Golang there are numerous ways to return and handle errors Namely:=

  • Casting Errors
  • Error wrapping mechanism
  • Panic, defer and recover

Different methods of error handling in Go Func

Method 1:- Casting Errors

Casting errors is a way of defining custom and expected errors, with golang we can make use of erros.Isand errors.As() error functions to cast different types of errors. i.e,errors.Is we create a custom error of a particular type and 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"
)

var fileNotFound = errors.New("The file doesn't  exist")

func ReadFile(file string) error {
	dataFile, readErr := os.ReadFile(file)
	if readErr != nil {
		if errors.Is(readErr, fs.ErrNotExist) {
			return fmt.Errorf("this fileName %s  doesn't exist ", file)
		} else {
			return fmt.Errorf("Error  occured while opening the file : %w", readErr)
		}
	}
	fmt.Println(string(dataFile))
	return nil
}
func main() {
	fileName := os.Args[1]
	if fileName != "" {
		resultsError := ReadFile(fileName)
		if resultsError != nil {
			fmt.Printf("%v", resultsError)
		}
	} else {
		fmt.Println("the file name cant be empty")
	}
}

Output:

$ go run main.go "new"
this fileName new  doesn't exist

Explanation:- We are using errors.Is(readErr, fs.ErrNotExist) {} to check if the file passed exist, if it doesn’t exist we return custom message as shown above. we can also use the custom error message such as errors.New() to create expected error and handle it as errors.Is(readErr, fileNotFound) {} the return values will be the same.

ALSO READ: Golang Print Struct Variables [SOLVED]

Method 2:- Error wrapping

Wrapping is a way of using other errors within a function to provide more context and detailed error messages.

fmt.Error() function enable us to create a wrapped errors with use of %w flag. The %w flag is used for inspecting and unwrapping errors.
In this subtitles we can incorporate other functions from errors package used to handle errors, namely:- errors.As, errors.Is, errors.Unwrap functions. errors.As is used to cast a specific error type, i.e func As(err error, target any) bool{}, also, the errors.Unwrap is used to inspect and expose the underlying errors in a program,i.e func (e *PathError)Unwrap()error{ return e.Err}, Furthermore the errors.Is mostly for comparing error value against the sentinel value if is true or false, i.e func Is(err,target error) bool{}.

Example of Error Wrapping

package main

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

func ReadFile(file string) error {
	dataFile, readErr := os.ReadFile(file)
	var pathError *os.PathError
	if readErr != nil {
		if errors.As(readErr, &pathError) {
			return fmt.Errorf("this fileName %s  doesn't exist and failed  opening file at this path %v", file, pathError.Path)
		}
		return fmt.Errorf("Error  occured while opening the file : %w", readErr)

	}
	fmt.Println(string(dataFile))
	return nil
}
func main() {
	fileName := os.Args[1]
	if fileName != "" {
		resultsError := ReadFile(fileName)
		if resultsError != nil {
			fmt.Printf("%v", resultsError)
		}
	} else {
		fmt.Println("the file name can't be empty")
	}
}

Output:

With the file attached ensure you replace the ReadFile(«test.txt»)

$ go run main.go
Hello

without file attached

$ go run main.go ""
the file name can't be empty
$ go run main.go next.txt
this fileName news.txt  doesn't exist and failed opening file at this path news.txt

Explanation:- In the above code we have used fmt.Errorf() functions to format the error message to be displayed and wrapping error using a custom error message with wrap function errors.Is() which checks if the path exists. You can avoid unnecessary error wrapping and handle it once.

ALSO READ: Golang SQLite3 Tutorial [With Examples]

Method-3: Using Panic, Defer and Recover

We have covered this topic in detail in a separate article Golang panic handing [capture, defer, recover, log]

Summary

At this point in this article, you have learned various ways to return and handle errors in the Golang function. In Go, Errors are considered to be a very lightweight piece of data that implements the Error interface. Custom errors in Go help in debugging and signaling where the error has occurred from. Error tracing is easy as compared to other programming languages. Golang application development, error handling is very critical and helps one not only during debugging but also to monitor the application behavior. We recommend you to read more about panic, recover and defer mechanism of error handling as well.

References

error-handling in Go
Working with errors in golang
Errors

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

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


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

Механизм обработки ошибок в 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-разработчика, чтобы не допускать их в начале своего карьерного пути.

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.

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
}

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

error

error это интерфейс. И как большинство интерфейсов в Go, определение error краткое и простое:

type error interface {
    Error() string
}

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

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

err := errors.New("emit macho dwarf: elf header corrupted")

const name, id = "bimmler", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)

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

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

Значение такой ошибки будет содержать операцию, путь и ошибку.

Инициализируются они таким образом:

...
return nil, &PathError{"open", name, syscall.ENOENT}
...
return nil, &PathError{"close", file.name, e}

Обработка может иметь стандартный вид:

_, err := os.Open("---")
if err != nil{
    fmt.Println(err)
}
// open ---: The system cannot find the file specified.

А вот если есть необходимость получить дополнительную информацию, то можно распаковать error в *os.PathError:

_, err := os.Open("---")
if pe, ok := err.(*os.PathError);ok{
    fmt.Printf("Err: %sn", pe.Err)
    fmt.Printf("Op: %sn", pe.Op)
    fmt.Printf("Path: %sn", pe.Path)
}
// Err: The system cannot find the file specified.
// Op: open
// Path: ---

Этот же подход можно применять если функция может вернуть несколько различных типов ошибок.
play

Объявление нескольких типов ошибок, каждая имеет свои данные:

code

type ErrTimeout struct {
    Time time.Duration
    Err  error
}
func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() }

type ErrPermission struct {
    Status string
    Err  error
}
func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }

Функция которая может вернуть эти ошибки:

code

func proc(n int) error {
    if n <= 10 {
        return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")}
    } else if n >= 10 {
        return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")}
    }
    return nil
}

Обработка ошибок через приведения типов:

code

func main(){
    err := proc(11)
    if err != nil {
        switch e := err.(type) {
        case *ErrTimeout:
            fmt.Printf("Timeout: %sn", e.Time.String())
            fmt.Printf("Error: %sn", e.Err)
        case *ErrPermission:
            fmt.Printf("Status: %sn", e.Status)
            fmt.Printf("Error: %sn", e.Err)
        default:
            fmt.Println("hm?")
            os.Exit(1)
        }
    }
}

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

В примере ниже, прерываем чтение и продолжаем работу приложения, когда ошибка равна io.EOF или закрываем приложения при любых других ошибках.

func main(){
    reader := strings.NewReader("hello world")
    p := make([]byte, 2)

    for {
        _, err := reader.Read(p)
        if err != nil{
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
    }
}

Это эффективно, поскольку ошибки создаются только один раз и используются многократно.

stack trace

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

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

Для вывода трассировки в стандартный выводов можно воспользоваться debug.PrintStack():

func main(){
    foo()
}

func foo(){
    bar()
}
func bar(){
    debug.PrintStack()
}

Как результат в Stderr будет записано такая информация:

stack

goroutine 1 [running]:
runtime/debug.Stack(0x1, 0x7, 0xc04207ff78)
        .../Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
        .../Go/src/runtime/debug/stack.go:16 +0x29
main.bar()
        .../main.go:13 +0x27
main.foo()
        .../main.go:10 +0x27
main.main()
        .../main.go:6 +0x27

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

b := debug.Stack()
fmt.Printf("Trace:n %sn", b)

Есть еще один момент, если мы сделаем вот так:

go bar()

то на выходе получим такую информацию:

main.bar()
        .../main.go:19 +0x2d
created by main.foo
        .../main.go:14 +0x3c

У каждой горутины отдельный стек, соответственно, мы получаем только его дамп. Кстати, о своих стеках у горутин, с этим еще связана работа recover, но об этом чуть позже.
И так, что бы увидеть информацию по всем горутинам, можно воспользоваться runtime.Stack() и передать вторым аргументом true.

func bar(){
    buf := make([]byte, 1024)
    for {
        n := runtime.Stack(buf, true)
        if n < len(buf) {
            break
        }
        buf = make([]byte, 2*len(buf))
    }
    fmt.Printf("Trace:n %sn", buf)
}

stack

Trace:
 goroutine 5 [running]:
main.bar()
        .../main.go:21 +0xbc
created by main.foo
        .../main.go:14 +0x3c

goroutine 1 [sleep]:
time.Sleep(0x77359400)
        .../Go/src/runtime/time.go:102 +0x17b
main.foo()
        .../main.go:16 +0x49
main.main()
        .../main.go:10 +0x27

Добавим в ошибку эту информацию и тем самым сильно повысим ее информативность.
Например так:

type ErrStack struct {
    StackTrace []byte
    Err  error
}
func (e *ErrStack) Error() string {
    var buf bytes.Buffer
    fmt.Fprintf(&buf, "Error:n %sn", e.Err)
    fmt.Fprintf(&buf, "Trace:n %sn", e.StackTrace)
    return buf.String()
}

Можно добавить функцию для создания этой ошибки:

func NewErrStack(msg string) *ErrStack {
    buf := make([]byte, 1024)
    for {
        n := runtime.Stack(buf, true)
        if n < len(buf) {
            break
        }
        buf = make([]byte, 2*len(buf))
    }
    return &ErrStack{StackTrace: buf, Err: errors.New(msg)}
}

Дальше с этим уже можно работать:

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

func foo() error{
    return bar()
}
func bar() error{
    err := NewErrStack("error")
    return err
}

stack

Error:
 error
Trace:
 goroutine 1 [running]:
main.NewErrStack(0x4c021f, 0x5, 0x4a92e0)
        .../main.go:41 +0xae
main.bar(0xc04207ff38, 0xc04207ff78)
        .../main.go:24 +0x3d
main.foo(0x0, 0x48ebff)
        .../main.go:21 +0x29
main.main()
        .../main.go:11 +0x29

Соответственно ошибку и трейс можно рзаделить:

func main(){
    err := foo()

    if st, ok := err.(*ErrStack);ok{
        fmt.Printf("Error:n %sn", st.Err)
        fmt.Printf("Trace:n %sn", st.StackTrace)
    }
}

И конечно уже есть готовые решение. Одно из них, это пакет https://github.com/pkg/errors. Он позволяет создавать новую ошибку, которая уже будет содержать стек трейс, а можно добавлять трейс и/или дополнительное сообщения к уже существующей ошибке. Плюс удобное форматирование вывода.

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

func main(){
    err := foo()
    if err != nil {
        fmt.Printf("%+v", err)
    }
}

func foo() error{
    err := bar()
    return errors.Wrap(err, "error2")
}
func bar() error{
    return errors.New("error")
}

stack

error
main.bar
        .../main.go:20
main.foo
        .../main.go:16
main.main
        .../main.go:9
runtime.main
        .../Go/src/runtime/proc.go:198
runtime.goexit
        .../Go/src/runtime/asm_amd64.s:2361
error2
main.foo
        .../main.go:17
main.main
        .../main.go:9
runtime.main
        .../Go/src/runtime/proc.go:198
runtime.goexit
        .../Go/src/runtime/asm_amd64.s:2361

%v выведет только сообщения

error2: error

panic/recover

Паника(aka авария, aka panic), как правило, сигнализирует о наличии неполадок, из-за которых система (или конкретная подсистема) не может продолжать функционировать. В случае вызова panic среда выполнения Go просматривает стек, пытаясь найти для нее обработчик.

Необработанные паники прекращают работу приложения. Это принципиально отличает их от ошибок, которые позволяют не обрабатывать себя.

В вызов функции panic можно передать любой аргумент.

panic(v interface{})

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

panic(errors.New("error"))

Восстановление после аварии в Go основывается на отложенном вызове функций, он же defer. Такая функция гарантировано будет выполнена в момент возврата из родительской функции. Не зависимо от причины — оператор return, конец функции или паника.

А вот уже функция recover дает возможность получить информацию об аварии и остановить раскручивание стека вызовов.
Типичный пример вызова panic и обработчик:

func main(){
    defer func() {
        if err := recover(); err != nil{
            fmt.Printf("panic: %s", err)
        }
    }()
    foo()
}

func foo(){
    panic(errors.New("error"))
}

recover возвращает interface{} (тот самый который передаем в panic) или nil, если не было вызова panic.

Рассмотрим еще один пример обработки аварийных ситуаций. У нас есть некоторая функция в которую мы передаем например ресурс и которая в теории может вызвать панику.

func bar(f *os.File) {
    panic(errors.New("error"))
}

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

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

Такую задачу можно решить с помощью defer, recover и замыкания:

func foo()(err error) {
    file, _ := os.Open("file")
    defer func() {
        if r := recover(); r != nil {
            err = r.(error) // обрабатываем аварийную ситуацию, распаковываем если знаем, что в панике ошибка
            // err := errors.New("trapped panic: %s (%T)", r, r) // или создаем свою ошибку
        }
        file.Close() // закрываем файл
    }()

    bar(file)

    return err
}

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

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

В подобных случаях добавляют функцию обертку в которой вызывается целевая функция и в случае ошибки вызывается panic.

В Go обычно такие функции с префиксом Must:

// MustCompile is like Compile but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

// Must is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil. It is intended for use in variable initializations
// such as
//  var t = template.Must(template.New("name").Parse("html"))
func Must(t *Template, err error) *Template {
    if err != nil {
        panic(err)
    }
    return t
}

Стоит помнить еще про один момент, связанный с panic и горутинами.

Часть тезисов из того что обсудили выше:

  • Для каждой горутины выделяется отдельный стек.
  • При вызове panic, в стеке ищется recover.
  • В случае, когда recover не найдет, завершается все приложение.

Обработчик в main не перехватит панику из foo и программа аварийно завершится:

func main(){
    defer func() {
        if err := recover(); err != nil{
            fmt.Printf("panic: %s", err)
        }
    }()

    go foo()

    time.Sleep(time.Minute)
}
func foo(){
    panic(errors.New("error"))
}

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

type f func()

func Def(fn f) {
    go func() {
        defer func() {
            if err := recover(); err != nil {
                log.Println("panic")
            }
        }()

        fn()
    }()
}

func main() {
    Def(foo)

    time.Sleep(time.Minute)
}

func foo() {
    panic(errors.New("error"))
}

handle/check

Возможно в будущем нас ждут изменения в обработки ошибок. Ознакомится с ними можно по ссылкам:
go2draft
Обработка ошибок в Go 2

На сегодня все. Спасибо!

Понравилась статья? Поделить с друзьями:
  • Golang package error
  • Golang new error handling
  • Glowstorm gta 5 как исправить
  • Glow storm gta 5 как исправить
  • Glow device general error