Custom errors in golang and pointer receivers

2020-02-10 08:11发布

问题:

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?

回答1:

First, the blog post you linked and took your example from, appError is not an error. It's a wrapper that carriers 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).



回答2:

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.



回答3:

We can look at this from the error handling's perspective, instead of the error creation.

Error Definiton Side's Story

type ErrType1 struct {}

func (e *ErrType1) Error() string {
    return "ErrType1"
}

type ErrType2 struct {}

func (e ErrType2) Error() string {
    return "ErrType1"
}

Error Handler Side's Story

err :=  someFunc()
switch err.(type) {
case *ErrType1
   ...
case ErrType2, *ErrType2
   ...
default
   ...
}

As you can see, if you implements a error type on a value receiver, then when you are doing the type assertion, you need to worry about both cases.

For ErrType2, both &ErrType2{} and ErrType2{} satisfy the interface.

Because someFunc returns an error interface, you never know if it returns a struct value or a struct pointer, especially when someFunc isn't written by you.

Therefore, by using a pointer receiver doesn't stop a user from returning a pointer as an error.

That been said, all other aspects such as Stack vs. Heap (memory allocation, GC pressure) still apply.

Choose your implementation according to your use cases.

In general, I prefer to a pointer receiver for the reason I demonstrated above. I prefer to Friendly API over performance and sometimes, when error type contains huge information, it's more performant.



回答4:

No :)

https://blog.golang.org/error-handling-and-go#TOC_2.

Go interfaces allow for anything that complies with the error interface to be handled by code expecting error

type error interface {
    Error() string
}

Like you mentioned, If you don't plan to modify state there is little incentive to pass around pointers:

  • allocating to heap
  • GC pressure
  • Mutable state and concurrency, etc

On a random rant , Anecdotally, I personally think that seeing examples like this one are why new go programers favor pointer receivers by default.



回答5:

The tour of go explains the general reasons for pointer receivers pretty well:

https://tour.golang.org/methods/8

There are two reasons to use a pointer receiver.

The first is so that the method can modify the value that its receiver points to.

In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.