The crash that takes down the server
You're building a handler that greets a user by name. The database lookup returns a pointer to a User struct. Most of the time, the user exists. Once in a while, the ID is wrong, and the lookup returns nil. You forget to check. The request comes in, the code runs fmt.Println(user.Name), and the process dies with a panic. The server goes down for everyone, not just the one bad request.
This is a nil pointer dereference. It's the most common runtime panic in Go. The fix is simple: check the pointer before you use it. The discipline is harder: treat every pointer as potentially nil until you prove otherwise.
Pointers, addresses, and the zero value
A pointer holds a memory address. It tells the program where to find a value. nil is the zero value for pointers. It means "I don't point anywhere." Dereferencing a nil pointer is like trying to read a letter from a house that doesn't exist. The runtime can't fetch the data, so it panics.
Go gives every variable a zero value. For integers, it's 0. For strings, it's "". For pointers, it's nil. When you declare a pointer without initializing it, you get nil.
package main
import "fmt"
type Config struct {
// Name holds the configuration name.
Name string
}
func main() {
// var c *Config declares a pointer but doesn't allocate memory.
// The zero value for a pointer is nil.
var c *Config
// Check if c is nil before accessing fields.
// Accessing c.Name when c is nil causes a panic.
if c != nil {
fmt.Println(c.Name)
} else {
fmt.Println("Config is not set")
}
}
Nil is a value. Check it like any other value.
What happens at runtime
The compiler does not catch nil dereferences. It assumes you know what you're doing. When you write ptr.Field, the compiler emits instructions to read the address stored in ptr, then read the field from that address. If ptr is nil, the address is zero. The runtime detects the invalid memory access and stops the program.
This happens at runtime, not compile time. The panic message tells you exactly which line caused the crash. The output looks like this: panic: runtime error: invalid memory address or nil pointer dereference. The stack trace follows, showing the function and line number. The trace helps you find the exact spot where the code tried to use a nil pointer.
Go's error handling convention reinforces defensive coding. Functions return errors explicitly. You check the error before using the result. This habit transfers to pointers. If a function returns a pointer, ask yourself: can this be nil? If yes, check it. The verbosity of if err != nil is a feature. It forces you to acknowledge the failure case. The same discipline applies to nil pointers.
Trust the panic trace. It points to the problem. Fix the check, not the symptom.
Real-world patterns: lookups and maps
In production code, nil pointers usually come from lookups. A database query returns nothing. A map key is missing. A slice index is out of bounds. Each case has a standard pattern.
Database lookups often return a pointer. If the record exists, you get a pointer to the struct. If not, you get nil. The caller must check.
package main
import (
"fmt"
"net/http"
)
type User struct {
// ID identifies the user.
ID int
// Name holds the display name.
Name string
}
// findUser looks up a user by ID.
// It returns nil if the user is not found.
func findUser(id int) *User {
// Simulate a database lookup.
// Return nil to represent a missing record.
if id == 42 {
// Allocate a new User on the heap and return a pointer.
return &User{ID: 42, Name: "Alice"}
}
// Return nil when no user matches the ID.
return nil
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Parse ID from request (simplified).
id := 42
// Call the lookup function.
user := findUser(id)
// Check for nil before using the pointer.
// This pattern prevents the panic on missing users.
if user == nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// Safe to access fields now.
fmt.Fprintf(w, "Hello %s", user.Name)
}
Maps use a different pattern. They return a boolean to tell you if the key exists. This is the comma-ok idiom.
package main
import "fmt"
func main() {
// Create a map of user IDs to names.
users := map[int]string{
1: "Alice",
2: "Bob",
}
// Access a key that exists.
// The comma-ok idiom returns the value and a boolean.
name, ok := users[1]
if ok {
fmt.Println(name)
}
// Access a key that does not exist.
// ok is false, and name is the zero value of string.
name, ok = users[99]
if !ok {
fmt.Println("user not found")
}
}
The comma-ok idiom is safer than checking the value. If the map stores pointers, the value might be nil even if the key exists. The boolean tells you the truth.
Use the comma-ok idiom for maps. Check the boolean, not the value.
The interface trap
The interface trap is the most common source of nil panics in experienced code. Beginners check the pointer. Experienced developers check the interface and still get burned. An interface value is a pair: a type and a value. When you assign a nil pointer to an interface, the type is set, but the value is nil. The interface itself is not nil. The check if iface != nil passes. The method call panics.
package main
import "fmt"
type Stringer interface {
// String returns the string representation.
String() string
}
type MyString string
// String implements Stringer.
func (s MyString) String() string {
return string(s)
}
func main() {
// var s Stringer is a nil interface.
var s Stringer
// Assign a nil pointer of a concrete type.
// The interface now holds a type and a nil value.
s = (*MyString)(nil)
// s != nil is true because the interface has a type.
// Calling a method panics because the value is nil.
if s != nil {
fmt.Println(s.String()) // panics
}
}
The compiler rejects this with panic: runtime error: invalid memory address or nil pointer dereference. The stack trace points to the method call. The check s != nil looked correct, but it only checked the interface, not the underlying pointer.
To avoid this, check the concrete value if you need to distinguish between a nil interface and a non-nil interface holding a nil pointer. Or better, avoid returning nil pointers wrapped in interfaces. Return a boolean or an error instead.
Interfaces hide nil pointers. Check the value, not just the interface.
Zero values and structs
Pointers are useful for sharing state and representing absence. They're not always necessary. Structs have zero values. A struct variable is valid even if you haven't initialized it. The fields get their zero values. You can access fields safely.
package main
import "fmt"
type Config struct {
// Host holds the server address.
Host string
// Port holds the server port.
Port int
}
func main() {
// var c Config creates a struct with zero values.
// Host is empty string, Port is 0.
var c Config
// Accessing fields is safe even with zero values.
// No nil pointer risk here.
fmt.Println(c.Host)
fmt.Println(c.Port)
}
This pattern eliminates nil checks. If you need to represent "not configured," use a boolean flag or a sentinel value. Don't use a nil pointer if a zero-value struct works.
The receiver naming convention applies here too. Methods on structs use short names matching the type. (c Config) GetHost(). The name is one or two letters. This makes code readable. It doesn't affect nil safety, but it's part of Go style.
Gofmt ensures consistent formatting. It doesn't check for nil. It formats the code so you can focus on logic. Trust gofmt. Argue logic, not formatting.
Nil channels block
Nil pointers panic. Nil channels behave differently. Sending or receiving on a nil channel blocks forever. The runtime doesn't panic. The goroutine hangs. This causes leaks.
A goroutine waiting on a nil channel never returns. If you pass a nil channel to a worker, the worker blocks. The program stalls. Use a select with a context to avoid hanging on nil channels. Context cancellation provides a way out.
Context is plumbing. Run it through every long-lived call site. The worst goroutine bug is the one that never logs.
When to use pointers and when to avoid them
Go gives you choices. Pick the right tool for the job.
Use a nil check when a function returns a pointer that might be absent, like a database lookup or a map access.
Use a value type instead of a pointer when the value is always present and small, like a string or integer. Passing a value avoids nil entirely.
Use a pointer when you need to modify the value in place or share state between multiple parts of the program.
Use the zero value of a struct when you can represent "empty" with default fields instead of nil. A struct with empty strings is safer than a nil pointer to a struct.
Use the comma-ok idiom when accessing maps. The boolean tells you if the key exists, regardless of the value.
Use panic only for unrecoverable errors in development, not for control flow. Let the program crash if the state is impossible.
Don't pass a *string. Strings are already cheap to pass by value. Pointers to strings add indirection and nil risk without benefit.
Accept interfaces, return structs. This mantra keeps your code flexible. Functions accept interfaces so callers can pass any implementation. Functions return structs so callers get concrete data. This reduces the chance of returning nil interfaces.
Use values for data. Use pointers for sharing and absence.