Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom errors in golang and pointer receivers

Reading about value receivers vs pointer receivers across the web and stackoverflow, I understand the basic rule to be: If you don't plan to modify the receiver, and the receiver is relatively small, there is no need for pointers.

Then, reading about implementing the error interface (eg. https://blog.golang.org/error-handling-and-go), I see that examples of the Error() function all use pointer receiver.

Yet, we are not modifying the receiver, and the struct is very small.

I feel like the code is much nicer without pointers (return &appError{} vs return appError{}).

Is there a reason why the examples are using pointers?

like image 945
Nathan H Avatar asked May 14 '18 14:05

Nathan H


People also ask

What is pointer receiver in Golang?

Pointer receiver passes the address of a type to the function. The function stack has a reference to the original object. So any modifications on the passed object will modify the original object.

How do you define a new error in Golang?

Creating custom errors using the New function. Adding more information to the error using Errorf. Providing more information about the error using struct type and fields. Providing more information about the error using methods on struct types.

What type is error in Golang?

error is a built-in interface type in Go. An error variable represents any value that can describe itself as a string . The interface consists of a single function, Error() , that returns a string error message.

What is Golang error handling?

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)


2 Answers

First, the blog post you linked and took your example from, appError is not an error. It's a wrapper that carries an error value and other related info used by the implementation of the examples, they are not exposed, and not appError nor *appError is ever used as an error value.

So the example you quoted has nothing to do with your actual question. But to answer the question in title:

In general, consistency may be the reason. If a type has many methods and some need pointer receiver (e.g. because they modify the value), often it's useful to declare all methods with pointer receiver, so there's no confusion about the method sets of the type and the pointer type.

Answering regarding error implementations: when you use a struct value to implement an error value, it's dangerous to use a non-pointer to implement the error interface. Why is it so?

Because error is an interface. And interface values are comparable. And they are compared by comparing the values they wrap. And you get different comparison result based what values / types are wrapped inside them! Because if you store pointers in them, the error values will be equal if they store the same pointer. And if you store non-pointers (structs) in them, they are equal if the struct values are equal.

To elaborate on this and show an example:

The standard library has an errors package. You can create error values from string values using the errors.New() function. If you look at its implementation (errors/errors.go), it's simple:

// 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
}

The implementation returns a pointer to a very simple struct value. This is so that if you create 2 error values with the same string value, they won't be equal:

e1 := errors.New("hey")
e2 := errors.New("hey")
fmt.Println(e1, e2, e1 == e2)

Output:

hey hey false

This is intentional.

Now if you would return a non-pointer:

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

type errorString struct {
    s string
}

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

2 error values with the same string would be equal:

e1 = New("hey")
e2 = New("hey")
fmt.Println(e1, e2, e1 == e2)

Output:

hey hey true

Try the examples on the Go Playground.

A shining example why this is important: Look at the error value stored in the variable io.EOF:

var EOF = errors.New("EOF")

It is expected that io.Reader implementations return this specific error value to signal end of input. So you can peacefully compare the error returned by Reader.Read() to io.EOF to tell if end of input is reached. You can be sure that if they occasionally return custom errors, they will never be equal to io.EOF, this is what errors.New() guarantees (because it returns a pointer to an unexported struct value).

like image 197
icza Avatar answered Sep 27 '22 21:09

icza


Errors in go only satisfy the error interface, i.e. provide a .Error() method. Creating custom errors, or digging through Go source code, you will find errors to be much more behind the scenes. If a struct is being populated in your application, to avoid making copies in memory it is more efficient to pass it as a pointer. Furthermore, as illustrated in The Go Programming Language book:

The fmt.Errorf function formats an error message using fmt.Sprintf and returns a new error value. We use it to build descriptive errors by successively prefixing additional context information to the original error message. When the error is ultimately handled by the program’s main function, it should provide a clear causal chain from the root problem to the overall failure, reminiscent of a NASA accident investigation:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

Because error messages are frequently chained together, message strings should not be capitalized and newlines should be avoided. The resulting errors may be long, but they will be self-contained when found by tools like grep.

From this we can see that if a single 'error type' holds a wealth of information, and on top of this we are 'chaining' them together to create a detailed message, using pointers will be the best way to achieve this.

like image 22
brun Avatar answered Sep 27 '22 23:09

brun