Go Nil Pointer Dereference Problem with FindXXX
The Problem
A nil pointer dereference is a well-known runtime error to avoid. The cause is simple to explain: a pointer to a struct is passed to or returned from a function. Accessing the struct's fields or methods can cause a runtime panic if the pointer turns out to be nil.
The FindXXX Pattern
In our code logic, there are often cases where an identifier maps to an in-memory representation of a struct or object. These objects are frequently stored in a map, but they can also be retrieved or reconstructed from a file or an endpoint. When this object needs to be passed to relevant functions, instead of passing the object directly, some methods may rely on passing the identifier instead.
For example, in the context of a library system:
package main
import "fmt"
type Book struct {
Title string
Author string
}
type Library struct {
books map[string]*Book
}
func (l *Library) AddBook(title, author string) {
l.books[title] = &Book{Title: title, Author: author}
}
func (l *Library) FindBook(title string) *Book {
if book, ok := l.books[title]; !ok {
return nil
} else {
return book
}
}
func main() {
library := Library{books: make(map[string]*Book)}
library.AddBook("Your Code as a Crime Scene", "Adam Tornhill")
// exist and well
book := library.FindBook("Your Code as a Crime Scene")
fmt.Printf("Book: %s, Author: %s\n", book.Title, book.Author)
// not exist and panic
book = library.FindBook("The Phoenix Project")
fmt.Printf("Book: %s, Author: %s\n", book.Title, book.Author)
}
In the code above, FindBook
provides a way to retrieve a book representation using the book title. The problem with this design is that nil checks are not enforced by the compiler, which can lead to carelessness in validating the returned object before accessing its fields.
The runtime panic:
Book: Your Code as a Crime Scene, Author: Adam Tornhill
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x100e81e0c]
goroutine 1 [running]:
main.main()
/Users/yong/Documents/GitHub/learn-go/library/main.go:46 +0x1bc
exit status 2
The Fix
In other languages, one might simply throw an exception when the requested object does not exist. However, in Go, returning an error is generally preferred as it provides a clearer indication of how the method should be used.
Of course, documenting the function with a comment to indicate the need for nil checks is better than nothing, but a more robust solution is to return an error explicitly:
func (l *Library) GetBook(title string) (*Book, error) {
if book, ok := l.books[title]; !ok {
return nil, fmt.Errorf("Book not found")
} else {
return book, nil
}
}
Summary
While this issue may seem trivial, it is more widespread and insidious than one might think. There are many scenarios where a FindXXX pattern (if such a term exists) can lead to the slippery slope of hidden nil pointer dereferences in the codebase.
Not returning an error and instead relying on a nil pointer is one part of the problem. The other issue is the practice of passing around identifiers, which leads to a loss of type safety—but that’s a topic for another day.