Golang error codes

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.

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.

Go does not have an Exception handling model as most of the main stream
languages. However, it uses the error interface type as the return type for any
error that is going to be returned from a function or method:

type error interface {
    Error() string
}

It is an interface type. An error variable represents any value
that can describe itself as a string. The most commonly-used error
implementation is in the errors package.

It can be instaciated in the following way:

func DivideBy(divider float64) (float64, error) {
    if divider <= 0 {
        return 0, errors.New("Divider cannot be zero or negative number.")
    }
    // implementation
}

The errors.New functions constructs an exported type errorString that
implements the Error interface:

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

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

It is pretty straighforward to implement your own error type that has additional
data.

The error model in Golang does not provide a way to find out, which function
returned the error. We should be aware and log the errors very carefully in
order to understand where this error occurred.

Fortunately, the Golang runtime provides a set of functions that we can use to
generate a stacktrace that we can trace down easily.

In the following paragraphs, we will explore the Planatir
stacktrace package that does this for
us.

Getting Started

The package captures a strategic places along the call stack and attaches relevant
contextual information like messages and variables. It is keeping stack traces
compact and maximally useful.

Installation

In order to use the package, we should install it first by using the well known
go get command:

$ go get github.com/palantir/stacktrace

Usage

The package provides a various functions to propagate and generate that
contextual information.

Error propagation

stacktrace.Propagate function replaces the usage of fmt.Errorf function. It
wraps an error to include line number information. This is going to be your
most common stacktrace call.

db, err := sql.Open("postgres", conninfo)
if err != nil {
   return stacktrace.Propagate(err, "Failed to connect %v", conninfo)
}

Creating errors

stacktrace.NewError creates a new error that includes line number information:

if amount <= 0 {
    return stacktrace.NewError("Expected amount %v to be positive number", arg)
}
Error Codes

Sometimes it’s useful to propagate an error code while unwinding the
stack. For instance, a RESTful API may use the error code to set the HTTP status
code. The type stacktrace.ErrorCode is used to name the set of error codes
relevant to your application:

const (
    ConnectionTimeout = stacktrace.ErrorCode(iota)
		ConnectionLost
)

The value stacktrace.NoCode is equal to math.MaxUint16, so avoid using
that. NoCode is the default value of errors that does not have explicitly set
error code.

You can use stacktrace.PropagateWithCode and stacktrace.NewErrorWithCode to
instaciated an error that has specific code:

db, err := sql.Open("postgres", conninfo)
if err != nil {
   return stacktrace.PropagateWithCode(err, ConnectionTimeout, "Failed to connect %v", conninfo)
}

You can extract the error code from the error by using stacktrace.GetCode
function:

data, err := fetch()
if err != nil {
  code := stacktrace.GetCode(err)
	if code == ConnectionTimeout {
	   return nil
	}
}

Verdict

The stacktrace package is very pleasant and easy to use. It comforms the Golang
idiomatic way of handling errors and provides us with additional contextual information
about the file and the line where the error occurred.

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

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

Премиум 👑 канал по Golang

Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎

Подписаться на канал

Уроки, статьи и Видео

Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.

Go в ВК

ЧАТ в Telegram

Содержание статьи

  • Исправление ошибок в Golang
  • Элегантная обработка ошибок
  • Запись данных в файле
  • Применяем defer — отложенные действия
  • Креативная обработка ошибок
  • Новые ошибки в программе на Golang
  • Причины каждой ошибки в Go
  • Настраиваемые типы ошибок
  • Множество ошибок в Golang
  • Утверждение типа Go
  • Принцип работы panic
  • Есть ли исключения в Golang?
  • Как использовать panic
  • Тонкости работы с panic в Go

Файл не найден, неверный формат, сервер недоступен. Что делает программа, когда что-то идет не так? Возможно, проблему можно решить, и тогда операции будут выполняться должным образом. Иногда лучше всего просто выйти и закрыть двери — или на крайний случай разбить окно и выскочить наружу.

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

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

В начала 18 века английский поэт Александр Поуп написал поэму, строчка которой известна по сей день: to err is human, то есть человеку свойственно ошибаться. Подумайте, как данную строку можно сравнить с программированием.

To err is human; to forgive, divine.

Александ Поуп, “An Essay on Criticism: Part 2”

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

Исправление ошибок в Golang

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

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

Чтобы продемонстрировать обработку ошибок, Листинг 1 вызывает функцию ReadDir. Если возникает ошибка, переменная err не будет равна nil, что заставит программу вывести ошибку и немедленно завершить работу. Ненулевое значение, переданное os.Exit, сообщает операционной системе, что произошла ошибка.

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

files, err := ioutil.ReadDir(«.»)

if err != nil {

    fmt.Println(err)

    os.Exit(1)

}

for _, file := range files {

    fmt.Println(file.Name())

}

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

При запуске Листинга 1 на Go Playground в выводе будет список директорий:

Для создания списка содержимого другой директории замените текущую директорию (".") в Листинге 1 названием другой директории вроде "etc". Список может содержать как файлы, так и директории. Вы можете использовать file.IsDir() для того, чтобы различить их.

Вопросы для проверки:

  1. Переделайте Листинг 1 для чтения воображаемой директории под названием "unicorns". Какая ошибка выйдет?
  2. Какое сообщение об ошибке выйдет при использовании ReadDir над файлом "/etc/hosts" вместо директории.

Элегантная обработка ошибок в Golang

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

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

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

Errors are values.
Don’t just check errors, handle them gracefully.
Don’t panic.
Make the zero value useful.
The bigger the interface, the weaker the abstraction.
interface{} says nothing.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.
Documentation is for users.
A little copying is better than a little dependency.
Clear is better than clever.
Concurrency is not parallelism.
Don’t communicate by sharing memory, share memory by communicating.
Channels orchestrate; mutexes serialize.

Rob Pike, Go Proverbs

Запись данных в файле

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

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

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

err := proverbs(«proverbs.txt»)

if err != nil {

    fmt.Println(err)

    os.Exit(1)

}

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

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    _, err = fmt.Fprintln(f, «Errors are values.»)

    if err != nil {

        f.Close()

        return err

    }

    _, err = fmt.Fprintln(f, «Don’t just check errors, handle them gracefully.»)

    f.Close()

    return err

}

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

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

Вопрос для проверки:

Почему функции должны возвращать ошибку вместо выхода из программы?

Применяем defer — отложенные действия в Golang

Убедиться, что файл правильно закрыт, можно с помощью ключевого слова defer. Go гарантирует, что все отложенные действия будут выполнены до возврата содержащей функции. В следующем листинге каждый возвращаемый оператор, следующий за defer, приведет к вызывающему методу f.Close().

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    defer f.Close()

    _, err = fmt.Fprintln(f, «Errors are values.»)

    if err != nil {

        return err

    }

    _, err = fmt.Fprintln(f, «Don’t just check errors, handle them gracefully.»)

    return err

}

На заметку: Поведение предыдущего кода похоже тому, что в Листинге 3. Изменение кода без изменения его поведения называется рефакторингом. Как и переосмысление первого черновика сочинения, рефакторинг является важным навыком для написания лучшего кода.

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

Ключевое слово defer облегчает процесс, однако проверка на наличие ошибок после каждой строки кода очень утомительна. Пришло время для креативности!

Вопрос для проверки:

Когда будет вызвано отсроченное действие?

Креативная обработка ошибок в Golang

15 января 2015 года в блоге Go была опубликована отличная статья об обработке ошибок. В статье описывается простой способ для записи в файл без повторения одинакового кода для обработки ошибок после каждой строчки.

Для применения данной техники вам понадобится объявить новый тип, что вызывается в Листинге 5. Если при записи safeWriter в файл возникает ошибка, он сохраняет ошибку вместо ее возвращения. Следующие попытки записи в одинаковый файл будут пропущены, если writeln видит, что раньше была ошибка.

type safeWriter struct {

    w   io.Writer

    err error // Место для хранения первой ошибки

}

func (sw *safeWriter) writeln(s string) {

    if sw.err != nil {

        return // Пропускает запись, если раньше была ошибка

    }

    _, sw.err = fmt.Fprintln(sw.w, s) // Записывает строку и затем хранить любую ошибку

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    defer f.Close()

    sw := safeWriter{w: f}

    sw.writeln(«Errors are values.»)

    sw.writeln(«Don’t just check errors, handle them gracefully.»)

    sw.writeln(«Don’t panic.»)

    sw.writeln(«Make the zero value useful.»)

    sw.writeln(«The bigger the interface, the weaker the abstraction.»)

    sw.writeln(«interface{} says nothing.»)

    sw.writeln(«Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.»)

    sw.writeln(«Documentation is for users.»)

    sw.writeln(«A little copying is better than a little dependency.»)

    sw.writeln(«Clear is better than clever.»)

    sw.writeln(«Concurrency is not parallelism.»)

    sw.writeln(«Don’t communicate by sharing memory, share memory by communicating.»)

    sw.writeln(«Channels orchestrate; mutexes serialize.»)

    return sw.err // Возвращает ошибку в случае ее возникновения

}

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

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

Роб Пайк, «Ошибки — это значения«

Перед вами элегантный способ обработки ошибок в Go.

Вопрос для проверки:

Если бы сообщение об ошибке в Листинге 6 сообщало файлу “Clear is better than clever.”, какие бы событие последовали за этим?

Новые ошибки в программе на Golang

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

Для демонстрации новых ошибок Листинг 7 создает основу для Судоку, что представляет собой сетку 9 на 9. Каждый квадрат сетки может содержать цифру от 1 до 9. Имплементация использует массив с фиксированным размером, ноль указывает на пустой квадрат.

const rows, columns = 9, 9

// Grid является сеткой Судоку

type Grid [rows][columns]int8

Пакет errors содержит функцию конструктора, что принимает строку для сообщения об ошибке. Используя ее, метод Set в «Листинге 8» может создать и возвратить ошибку "out of bounds".

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

func (g *Grid) Set(row, column int, digit int8) error {

    if !inBounds(row, column) {

        return errors.New(«out of bounds»)

    }

    g[row][column] = digit

    return nil

}

Функция inBounds в следующем листинге помогает убедиться, что row и column находятся в пределах границ сетки. Она не дает методу Set забиться лишними деталями.

func inBounds(row, column int) bool {

    if row < 0 || row >= rows {

        return false

    }

    if column < 0 || column >= columns {

        return false

    }

    return true

}

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

func main() {

    var g Grid

    err := g.Set(10, 0, 5)

    if err != nil {

        fmt.Printf(«An error occurred: %v.n», err)

        os.Exit(1)

    }

}

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

Всегда читайте сообщения об ошибках. Рассматривайте их как часть пользовательского интерфейса программы, будь он для конечных пользователей и разработчиков. Фраза «out of bounds» неплоха, но более точное «outside of grid boundaries» может быть лучше. А сообщение «error 37» вообще ни о чем не говорит.

Вопрос для проверки:

В чем преимущество защит от плохого ввода перед функцией?

Причины каждой ошибки в Go

Многие пакеты Go объявляют и экспортируют переменные для ошибок, которые они могут вернуть. Для использования этого с сеткой Судоку следующий листинг объявляет две переменные для ошибок на уровне пакета.

var (

    ErrBounds = errors.New(«out of bounds»)

    ErrDigit  = errors.New(«invalid digit»)

)

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

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

if !inBounds(row, column) {

    return ErrBounds

}

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

var g Grid

err := g.Set(0, 0, 15)

if err != nil {

    switch err {

    case ErrBounds, ErrDigit:

        fmt.Println(«Les erreurs de paramètres hors limites.»)

    default:

        fmt.Println(err)

    }

    os.Exit(1)

}

На заметку: Конструктор errors.New имплементируется через использование указателя, поэтому оператор switch в предыдущем примере сравнивает адреса памяти, текст не содержит сообщения об ошибке.

Задание для проверки:

Напишите функцию validDigit и используйте ее, чтобы убедиться, что метод Set принимает только цифры между 1 и 9.

Настраиваемые типы ошибок в Golang

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

Тип error является встроенным интерфейсом, как показано в следующем примере. Любой тип, что имплементирует метод Error() для возвращения строки, неявно удовлетворяет интерфейс. В качестве интерфейса возможно создать новые типы ошибок.

type error interface {

    Error() string

}

Множество ошибок в Golang

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

Вместо возвращения одной ошибки за раз, метод Set может сделать несколько проверок и вернуть все ошибки сразу. Тип SudokuError в Листинге 15 является срезом error. Он удовлетворяет интерфейсу error с методом, что соединяет ошибки вместе в одну строку.

На заметку: Принято, что настраиваемые типы ошибок вроде SudokuError заканчиваются словом Error. Иногда это просто слово Error вроде url.Error из пакета url.

type SudokuError []error

// Error возвращает одну или несколько ошибок через запятые.

func (se SudokuError) Error() string {

    var s []string

    for _, err := range se {

        s = append(s, err.Error()) // Конвертирует ошибки в строки

    }

    return strings.Join(s, «, «)

}

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

func (g *Grid) Set(row, column int, digit int8) error { // Возвращает тип ошибки

    var errs SudokuError

    if !inBounds(row, column) {

        errs = append(errs, ErrBounds)

    }

    if !validDigit(digit) {

        errs = append(errs, ErrDigit)

    }

    if len(errs) > 0 {

        return errs

    }

    g[row][column] = digit

    return nil // Возвращает nil

}

Если ошибок нет, метод Set возвращает nil. Это не изменилось по сравнению с Листингом 8, но важно отметить, что пустой срез errs здесь не возвращается. Для подробностей можете почитать об интерфейсах nil.

Сигнатура метода для Set также не изменилась по сравнению с Листингом 8. Всегда используйте тип интерфейса error при возвращении ошибок, а не конкретные типы вроде SudokuError.

Вопрос для проверки:

Что произойдет, если метод Set успешно вернет пустой срез errs?

Утверждение типа в Go

Так как Листинг 16 конвертирует SudokuError в тип интерфейса error перед его возвращением, может возникнуть вопрос, как получить доступ к отдельным ошибкам. Решением станет утверждение типа, или type assertion. Используя утверждение типа, вы можете конвертировать интерфейс в конкретный базовый тип.

Утверждение типа в Листинге 17 утверждает err для типа SudokuError через код err.(SudokuError). Это так и есть, то ok будет истинным, а err будет SudokuError, давая доступ к срезам ошибок в данном случае. Помните, что отдельные ошибки для SudokuError являются переменными ErrBounds и ErrDigit, что могут сравниваться в случае необходимости.

var g Grid

err := g.Set(10, 0, 15)

if err != nil {

    if errs, ok := err.(SudokuError); ok {

        fmt.Printf(«%d error(s) occurred:n», len(errs))

        for _, e := range errs {

            fmt.Printf(«- %vn», e)

        }

    }

    os.Exit(1)

}

В выводе предыдущего кода будут следующие ошибки:

2 error(s) occurred:

out of bounds

invalid digit

На заметку: Если тип удовлетворяет нескольким интерфейсам, утверждение типа также может конвертировать из одного интерфейса в другой.

Вопрос для проверки:

Что делает утверждение типа err.(SudokuError)?

Принцип работы panic в Golang

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

Есть ли исключения в Golang?

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

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

Исключения — это стиль обработки ошибок, который можно считать включенным. Часто они не занимают код, тогда как выбор обработки исключений может привлекать изрядное количество специализированного кода. Это связано с тем, что вместо использования существующих возможностей языка исключения обычно имеют специальные ключевые слова, такие как try, catch, throw, finally, raise, rescue, except и так далее.

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

Вопрос для проверки:

В чем преимущество значений ошибок Go по сравнению с исключениями?

Как использовать panic в Golang

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

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

panic(«Я забыл свое полотенце»)

На заметку: Хотя значения ошибок обычно предпочтительнее panic, panic часто лучше, чем os.Exit в том, что panic запустит любую отсроченную функцию, а os.Exit этого делать не станет.

В некоторых ситуациях Go предпочтет panic вместо значений ошибок, это может быть деление на ноль:

var zero int

_ = 42 / zero // Runtime error: integer divide by zero — целое число делится на ноль

Вопрос для проверки:

Как программа может использовать panic?

Тонкости работы с panic в Golang

Чтобы panic не привел к сбою программы, Go предоставляет функцию recover, что показано в Листинге 18.

Отсроченные функции выполняются перед возвращением функции, даже в том случае, если задействуется panic. Если отсроченная функция вызывает recover, panic остановится, и программа продолжит выполняться. В таком случае цель у recover похожа на catch, except и rescue в других языках.

defer func() {

    if e := recover(); e != nil { // Приходит в себя после panic

         fmt.Println(e) // Выводит: Я забыл свое полотенце

    }

}()

panic(«Я забыл свое полотенце») // Приводит к panic

Данный код использует анонимную функцию.

Вопрос для проверки:

Где может использоваться встроенная функция recover?

Заключение

  • Ошибки являются значениями, что внутренне оперируют с несколькими возвращаемыми значениями и другой частью языка Go;
  • Будучи креативным, можно найти множество способов для обработки ошибок;
  • Настраиваемые типы ошибок могут удовлетворить интерфейсу error;
  • Ключевое слово defer помогает выполнить очистку перед возвращением функции;
  • Утверждение типа может конвертировать интерфейс в конкретный тип или другой интерфейс;
  • Не паникуйте — изучите ошибку.

Итоговое задание для проверки:

В стандартной библиотеке Go есть функция для парсинга веб адресов (см. golang.org/pkg/net/url/#Parse). Отобразите ошибку, которая возникает, когда url.Parse используется для неправильного веб адреса вроде того, что содержит пробелы: https://a b.com/.

Используйте специальный символ %#v с Printf для изучения ошибки. Затем выполните утверждение типа *url.Error для получения доступа и вывода полей базовой структуры.

На заметку: URL, или Uniform Resource Locator — адрес страницы в Интернете.

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

E-mail: vasile.buldumac@ati.utm.md

Образование

Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники

  • 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
  • 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»

Понравилась статья? Поделить с друзьями:
  • Golang custom error type
  • Golang create new error
  • Golang catch error
  • Going into standby mode как исправить
  • Gog ошибка 0xc000007b