Golang - Interfaces - Interface Values and the nil Trap
Introduction
This is one of the most famous “gotchas” in Go and a classic interview question for assessing a deeper understanding of interfaces. To understand the trap, you first need to know what an interface value looks like in memory.
The Structure of an Interface Value
Think of an interface variable not as a single thing, but as a box containing two pieces of information:
- Type: A label that says, “Here’s the kind of thing I’m holding.” (e.g.,
*CustomError
). - Value: The actual data or value itself.
An interface variable is only truly nil
when both the type label and the value inside the box are empty.
Analogy: An interface is like a gift box.
- For the box to be truly empty (
nil
), it must be completely empty: no gift inside (nil value) and no gift tag on it (nil type). - If you put a gift tag on an empty box, the box itself is no longer considered empty. It’s a “box for a specific gift, which happens to be missing.”
An Interface "Box" in Memory
+-----------------+
| Type | --> (The "gift tag" describing the concrete type)
+-----------------+
| Value | --> (The "gift" or the actual data)
+-----------------+
The nil
Interface Trap
The trap happens when an interface box contains a nil
value but has a non-nil type tag. The box itself is not considered nil
, even though the value inside it is.
This can be very confusing because when you print it, it might look like nil
, but when you check if err != nil
, the result is true
.
The Classic Example: Returning a nil
Pointer as an error
Let’s imagine a function that can return a special kind of error, but sometimes there’s no error at all.
package main
import "fmt"
// CustomError is our special error type.
type CustomError struct {
Message string
}
// This method makes *CustomError a valid `error`.
func (e *CustomError) Error() string {
return e.Message
}
// This function tries to do something and might produce a CustomError.
func doSomething() *CustomError {
// Let's pretend everything went fine. We want to signal "no error."
// We do this by returning `nil` for the *CustomError type.
return nil
}
// This function calls doSomething() and returns the result as a standard `error`.
func run() error {
// doSomething() returns a value that is a `nil` pointer of type *CustomError.
// When we return this value as an `error`, Go puts it in the interface "box."
// The box gets the type tag *CustomError, but the value inside is nil.
return doSomething()
}
func main() {
err := run()
// This is the trap! The program will enter this `if` block.
// Why? Because `err` (the box) is not nil. It has a type tag on it.
if err != nil {
fmt.Println("Error occurred!")
fmt.Printf("The value inside the box is: %v\n", err)
fmt.Printf("But the box itself is not nil: %t\n", err != nil)
} else {
fmt.Println("Success!")
}
}
Output:
Error occurred!
The value inside the box is: <nil>
But the box itself is not nil: true
What happened? (In simple steps)
- The
doSomething
function returnsnil
, but specifically, it’s a “nil of type*CustomError
”. - The
run
function takes this typednil
and puts it into anerror
interface variable. - The Go runtime prepares the
error
interface “box”:- It attaches the type tag
*CustomError
to the box. - It puts the
nil
value inside the box.
- It attaches the type tag
- The box now has a tag, so the box itself is not
nil
. - The check
if err != nil
asks, “Is the box itself empty?” The answer is no (it has a tag), so the condition istrue
, and the program incorrectly thinks there’s an error.
How to Avoid the Trap
The fix is to be very direct. If you mean to return “no error,” you must return a truly empty interface box, not a box with a tag and an empty value.
The Fix:
func runFixed() error {
customErr := doSomething()
// Explicitly check if the value is nil.
if customErr == nil {
// If it is, return a truly empty `error` interface (a nil box).
return nil
}
// If it's not nil, return the box with the error inside.
return customErr
}
func main() {
err := runFixed()
if err != nil {
fmt.Println("Error occurred!", err)
} else {
// This now works as expected.
fmt.Println("Success!")
}
}
Output:
Success!
Interview Question: “What will if err != nil
do if err
is an interface holding a nil
pointer of a concrete type? Why?”
Answer: “The check err != nil
will be true
. An interface value is only nil
if both its internal type and value parts are nil
. When it holds a nil
pointer, its type part is set, which makes the interface itself non-nil. This is a common trap, especially when returning typed nil
pointers as an error
.”