If you've written Go for any amount of time, you've probably typed if err != nil hundreds of times. It’s the standard way to handle errors. But sometimes, this check behaves in a way that seems to defy logic, especially when you think you're returning a nil error. This "gotcha" moment almost always comes down to a misunderstanding of how Go's interfaces work, particularly when they are nil.
Let's dive into what a nil interface really is and how to avoid this common pitfall.
What Is an Interface, Really?
First, it's crucial to understand that an interface variable isn't a simple value. It's a container with two parts:
A type: This holds the type of the concrete data stored in the interface (e.g., *bytes.Buffer, *MyError).
A value: This holds the actual value or a pointer to the value (e.g., a pointer to a buffer, nil).
An interface variable is only truly nil when both of these parts are nil. This is the key to the whole mystery.
Let's look at a simple example without even touching errors yet.
package main
import (
"bytes"
"fmt"
"io"
)
func main() {
// io.Reader is an interface.
var r io.Reader
fmt.Println(r == nil) // true
// bytes.Buffer is a struct
var b *bytes.Buffer
fmt.Println(b == nil) // true
r = b
// r is now: (type=*bytes.Buffer, value=nil)
fmt.Println(r == nil) // false
}
When we assign b to r, the interface r gets a type (*bytes.Buffer), so it's no longer considered nil, even though the value it holds is a nil pointer.
The Common error Interface Pitfall
This behavior becomes a real problem when dealing with the error interface. Many people think of error as a built-in primitive type, but it's not. It's an interface defined as:
type error interface {
Error() string
}
Any type that implements the Error() string method satisfies the error interface. Now, let's see how our new understanding of interfaces can lead to a bug.
Imagine you have a custom error type and a function that might return it.
package main
import "fmt"
type MyError struct {
Path string
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s: %s", e.Path, e.Msg)
}
func processFile(path string) *MyError {
return nil
}
func main() {
var err error = processFile("test.txt")
if err != nil {
fmt.Printf("Oops, an error occurred: %v\n", err) // This line will run!
fmt.Printf("Type: %T, Value: %v\n", err, err)
} else {
fmt.Println("Everything is fine.")
}
}
Running this code produces:
Oops, an error occurred: <nil>
Type: *main.MyError, Value: <nil>
The err != nil check passes because the err variable, an error interface, was given a value (nil) that had a concrete type (*MyError). Since the interface's type part was set, the interface itself is no longer nil.
The Solution: Return the Interface
The fix is simple and is a cornerstone of good Go practice: functions should return the error interface type, not a concrete error type.
By changing the function signature, you ensure that when you return nil, you're returning a true nil interface, where both the type and value are nil.
Here’s the corrected code:
package main
import "fmt"
type MyError struct {
Path string
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s: %s", e.Path, e.Msg)
}
func processFileCorrectly(path string) error {
shouldFail := false
if shouldFail {
return &MyError{Path: path, Msg: "file not found"}
}
return nil
}
func main() {
err := processFileCorrectly("test.txt")
if err != nil {
fmt.Printf("Oops, an error occurred: %v\n", err)
} else {
fmt.Println("Everything is fine.") // This line runs now!
}
}
Key Takeaway
A nil pointer to a concrete type is not the same as a nil interface. An interface is only nil if both its internal type and value are nil.
To avoid this trap, always make your functions that can fail return the error type. This way, when you return nil, you're creating a proper "zeroed" interface that will correctly evaluate to true in a == nil check.
Resources
Go Class: 20 Interfaces & Methods in Detail
Top comments (0)