In my first project in Go, I implemented my own Error
struct
to communicate detailed error information beyond the simple error message that Go’s built-in error
interface
provides.
When I first implemented it, I did it as a struct
. I’ve since learned that it’s better as an interface
:
package myerr
type Code string
type Error interface {
error // implementations must satisfy error interface
Code() Code // project-specific error code
Cause() Error // the Error that caused this Error, if any
OrigErr() error // the original error
// ...
}
Aside on Error Codes
The reason I chose a
string
for error codes rather than an integer is because the only advantage of integers is that they make it easy for C-like code toswitch
on them. However, there are a few disadvantages of integer codes including:
- They’re not good for human readers: you have to look up the codes or the error provider has to include string descriptions (in which case the provider might as well have made the codes a string in the first place).
- You have to ensure that the codes from different subsystems don’t conflict.
Yes, there’s a small performance penalty for comparing strings as opposed to integers, but:
- Presumably, handling errors is exceptional, so it doesn’t matter that the code is a bit slower since it’s not critical-path.
- If you’re doing a REST application where error codes are sent as part of a JSON response via HTTP, then you’re already taking a performance hit for
itoa
ing andatoi
ing the integer code (not to mention parsing the entire JSON document) since it’s sent over the wire as text. Hence, a few extra string comparisons are noise.
Despite now being an interface
, Error
still has to be implemented by a struct
:
type MyError struct {
code Code
origErr error
cause *MyError
}
func (e *MyError) Cause() Error {
return e.cause
}
func (e *MyError) Code() Code {
return e.code
}
func (e *MyError) OrigErr() error {
return e.origErr
}
That all seems fairly straightforward. (Note: other functions for creating errors are elided here since they’re not relevant for this article; but they’re equally straightforward.)
The Problem
I was then writing some negative unit tests (those that ensure an error is detected and reported correctly), so I needed a way to compare two Error
s. Go has DeepEqual()
in its standard library, but, while it works, it only tells you if two objects are equal. For testing, if two objects aren’t equal, it’s helpful to know what specifically is not equal, so I wrote ErrorDeepEqual()
that returns an error
describing this:
func ErrorDeepEqual(e1, e2 myerror.Error) error {
if e1 == nil {
if e2 != nil {
return errors.New("e1(nil) != e2(!nil)")
}
return nil
}
if e2 == nil {
return errors.New("e1(!nil) != e2(nil)")
}
if e1.Code() != e2.Code() {
return fmt.Errorf("e1.Code(%v) != e2.Code(%v)", e1.Code(), e2.Code())
}
// ...
return ErrorDeepEqual(e1.Cause(), e2.Cause())
}
Since an Error
can have a cause, ErrorDeepEqual()
ends with a recursive call to check that the causes are equal. The problem was one test ending with a panic over a nil
pointer on this line:
if e1.Code() != e2.Code() { // panics here because e1 is nil
But the if
lines before this line check for nil
, so neither e1
nor e2
can be nil
here, right? How can e1 == nil
be false
yet e1
be nil
?
The Reason
It turns out that, in Go, this can happen and confuses enough people to the point where there’s an FAQ about it. A quick summary is that an interface
has two parts: a type and a value of said type. An interface
is nil
only if both the type and the value are nil
:
var i interface{} // i = (nil,nil)
fmt.Println(i == nil) // true
var p *int // p = nil
i = p // i = (*int,nil)
fmt.Println(i == nil) // false: (*int,nil) != (nil,nil)
fmt.Println(i == (*int)(nil)) // true : (*int,nil) == (*int,nil)
fmt.Println(i.(*int) == nil) // true : nil == nil
For now, we’ll set aside the rationale as to why Go works this way (and whether the rationale is a good rationale).
The Fix
In the mean time, I still had to figure out how I was getting a non-nil
Error
with a nil
value. It turns out that this is the culprit:
func (e *MyError) Cause() Error {
return e.cause // WRONG!
}
The problem is that whenever you assign to an interface
(either explicitly via assignment or implicitly via return
value), it takes on both the value and the concrete type. Once it takes on a concrete type, it will never compare equal to (the typeless) nil
. The fix is:
func (e *MyError) Cause() Error {
if e.cause == nil { // if typed pointer is nil ...
return nil // return typeless nil explicitly
}
return e.cause // else return typed non-nil
}
That is: when a function’s return type is an interface
and you can return a nil
pointer, you must check for nil
and return the literal nil
if the pointer is nil
.
The Rationale
There are several reasons why Go works this way. There’s a long discussion thread on the go-nuts mailing list. I’ve gone through it and distilled the main reasons.
The first is because Go allows any user-defined type to have methods defined for it, not just struct
types (or classes found in other languages). For example, we can define our own type of int
and implement the Stringer
interface
for it:
type Aint int // accounting int: print negatives in ()
func (n Aint) String() string {
if n < 0 {
return fmt.Sprintf("(%d)", -n)
}
return fmt.Sprintf("%d", n)
}
Now, let’s use a Stringer
interface
variable:
var n Aint // n = (Aint,0)
var s fmt.Stringer = n // s = (Aint,0)
fmt.Println(s == nil) // false: (Aint,0) != (nil,nil)
Here, s
is non-nil
because it refers to an Aint
— and the value just happens to be 0. There’s nothing noteworthy about a 0 value since 0 is a perfectly good value for an int
.
The second reason is that Go has very strong typing. For example:
fmt.Println(s == 0) // error: can't compare (Aint,0) to (int,0)
is a compile-time error since you can’t compare s
(that refers to an Aint
) to 0 (since a plain 0 is of type int
). You can, however, use a cast or type assertion:
fmt.Println(s == Aint(0)) // true: (Aint,0) == (Aint,0)
fmt.Println(s.(Aint) == 0) // true: 0 == 0
The third reason is that Go explicitly allows nil
for methods having pointer receivers:
type T struct {
// ...
}
func (t *T) String() string {
if t == nil {
return "<nil>"
}
// ...
}
Just as 0 is a perfectly good value for Aint
, nil
is a perfectly good value for a pointer. Since nil
doesn’t point to any T
, it can be used to implement default behavior for T
. Repeating the earlier example for Aint
but now for *T
:
var p *T // p = (*T,nil)
var s fmt.Stringer = p // s = (*T,nil)
fmt.Println(s == nil) // false: (*T,nil) != (nil,nil)
fmt.Println(s == (*T)(nil)) // true : (*T,nil) == (*T,nil)
fmt.Println(s.(*T) == nil) // true : nil == nil
we get analogous results for pointers — almost. (Can you spot the inconsistency?)
Where Go Goes Wrong
Unlike the earlier example with Aint
where comparing s
(of type Aint
) to 0 (of type int
) was an error:
fmt.Println(s == 0) // error: can't compare (Aint,0) to (int,0)
whereas comparing s
(of type *T
) to nil
(of type nil
) is OK:
fmt.Println(s == nil) // OK to compare
The problem is that, in Go, nil
is overloaded with two different meanings depending on context:
-
nil
refers to a pointer with 0 value. -
nil
refers to aninterface
with no type and 0 value.
Suppose Go had another keyword nilinterface
for case 2 and nil
were reserved exclusively for case 1. Then the above line should have been written as:
fmt.Println(s == nilinterface) // false: (*T,nil) != nilinterface
and it would be crystal clear that you were checking the interface
itself for no type and a 0 value and not checking the value of the interface
.
Furthermore, using the original nil
would result in an error (for the same reason you can’t compare an Aint
to a 0 without a cast or type assertion):
fmt.Println(s == nil) // what-if error: can't compare (*T,nil) to (nil,nil)
fmt.Println(s == (*T)(nil)) // still true: (*T,nil) == (*T,nil)
fmt.Println(s.(*T) == nil) // still true: nil == nil
The confusion in Go is that when new programmers write:
fmt.Println(s == nil) // checks interface for empty, not the value
they think they’re checking the interface’s value but they’re actually checking the interface itself for no type and 0 value. If Go had a distinct keyword like nilinterface
, all this confusion would go away.
Hacks
If you find all this bothersome, you can write a function to check an interface
for a nil
value ignoring its type (if any):
func isNilValue(i interface{}) bool {
return i == nil || reflect.ValueOf(i).IsNil()
}
While this works, it’s slow because reflection is slow. A much faster implementation is:
func isNilValue(i interface{}) bool {
return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0
}
This checks the value part of the interface
directly for zero ignoring the type part.
Top comments (7)
Wellcome aboard to the land of runtime panics and using
reflect
package in a statically typed language!!!This seems more explicit:
The panic I got wasn’t due to using reflection. As to your definition of the function, it’s supposed to be for any
interface
— it has nothing to do withError
.why do you need to generalize it to
interface{}
if you need in to work with the concreteError
interface? What is the point of static types if you eventually still usingreflect
orunsafe
?Because I prefer general solutions to narrow solutions. Suppose you have not one but N interfaces where N >> 1. Do you really want to write N different functions, one for each interface, that all do the same thing? If so, feel free — in your code-base.
But the actual fix (as I wrote) is not to let the interface become (*MyError,nil) in the first place. I added the Hacks section as a general solution to show it’s possible to check for a nil-valued interface in Go, but I never said it was preferable.
I never said I was using either. Again, the actual fix is as I described. I am not using the functions I described in the Hacks section.
Please read what I actually wrote and do not assume anything I did not write explicitly.
(*MyError,nil)
Nil as a value in interface if a hack of language designI don't think that a good idea to have N different nil interfaces in the first place
I admit I have not read your post after seeing the code examples because of obvious reasons.
Take it up with the Go authors, not me. I only described the way Go is and what the authors’ rationale for it being that way is. As I mentioned, it does actually have a use. But I'm not going to argue either for or against it — that’s outside the scope of my post.
In general, perhaps actually read posts before commenting on them. Specifically for my posts, you must read them before commenting on them; or please don’t comment on them at all. Thanks for your cooperation.
I have no idea what that means.
Absolutely! That's your right sir. I promise I won't ever either read of comment your valuable content.
Some comments have been hidden by the post's author - find out more