Go Nil Pointer Dereference — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code involving nil pointers. Identify the bug, explain why it panics, and provide the corrected code. Difficulty: Easy, Medium, Hard.
Bug 1 (Easy) — Chained Field Access Without Check¶
package main
import "fmt"
type Address struct {
City string
}
type Profile struct {
Address *Address
}
type User struct {
Profile *Profile
}
func main() {
u := &User{}
fmt.Println(u.Profile.Address.City)
}
Solution
**Bug**: `u.Profile` is nil (the zero value of `*Profile`). The expression `u.Profile.Address.City` reads `u.Profile`, then attempts to dereference it for `.Address` — panic on the first nil link. Output: **Fix** (option A — guards at each level):if u != nil && u.Profile != nil && u.Profile.Address != nil {
fmt.Println(u.Profile.Address.City)
} else {
fmt.Println("(no city)")
}
Bug 2 (Easy) — Map of Pointers, Missing Key¶
package main
import "fmt"
type User struct {
Name string
}
func main() {
users := map[string]*User{
"alice": {Name: "Alice"},
}
bob := users["bob"] // not present
fmt.Println(bob.Name)
}
Solution
**Bug**: `users["bob"]` returns `nil` because "bob" is not in the map. The zero value of the value type `*User` is `nil`. `bob.Name` then dereferences nil — panic. Output: **Fix** (option A — comma-ok form): **Fix** (option B — store values, not pointers): **Key lesson**: Reading a missing key from a map of pointers returns nil. Use comma-ok or store values.Bug 3 (Easy) — Unset Pointer Field¶
Solution
**Bug**: `Box{}` creates a Box with `p` defaulting to `nil` (the zero value of `*int`). `*b.p` then dereferences nil — panic. **Fix** (option A — initialize): **Fix** (option B — guard): **Fix** (option C — change the field type): **Key lesson**: Pointer fields default to nil. Either initialize them or check before use.Bug 4 (Easy) — Typed Nil Returned as Error¶
package main
import "fmt"
type MyErr struct{ msg string }
func (e *MyErr) Error() string {
return e.msg
}
func validate(x int) error {
var e *MyErr
if x < 0 {
e = &MyErr{msg: "negative"}
}
return e
}
func main() {
err := validate(5)
if err != nil {
fmt.Println("error:", err.Error())
} else {
fmt.Println("ok")
}
}
Solution
**Bug**: When `x = 5`, the function does NOT assign `e`. The variable stays as a nil `*MyErr`. Returning it wraps a nil pointer in the `error` interface — the interface is non-nil because it carries the type tag `*MyErr`. `err != nil` is true. `err.Error()` dispatches to `(*MyErr).Error` with a nil receiver, which then reads `e.msg` — panic. Output: **Fix** (return bare nil): **Fix** (alternative — nil-safe Error method): This avoids the panic, but `err != nil` is still misleadingly true. Always prefer returning bare nil. **Key lesson**: Never return a typed nil pointer when the function signature is an interface. Return `nil` directly.Bug 5 (Medium) — Method on Nil Pointer Reading Field¶
package main
import "fmt"
type Counter struct {
n int
}
func (c *Counter) Add(x int) {
c.n += x
}
func main() {
var c *Counter
c.Add(5)
fmt.Println(c.n)
}
Solution
**Bug**: `c` is nil. The method call `c.Add(5)` does not panic at the call site (pointer receiver methods can be invoked on nil receivers). But `Add`'s body says `c.n += x`, which reads and writes the field — panic. **Fix** (option A — initialize): **Fix** (option B — nil-safe method): A nil-safe `Add` is awkward (you can't mutate the unbacked struct). For setters, prefer to require non-nil and document. **Fix** (option C — return new Counter for "absent" case): **Key lesson**: Pointer-receiver methods invoked on nil panic when the body touches fields. Either initialize or make the method nil-safe.Bug 6 (Medium) — Nil Slice Index vs Nil Pointer¶
Solution
**Bug**: `s` is a nil slice — its length is 0. Indexing with `s[0]` is **out of range**, not a nil pointer dereference. The panic message differs: This is related to nil but distinct. A nil slice header has nil pointer + 0 len + 0 cap; iterating with `range` is fine, but indexing is bounds-checked. **Fix** (option A — guard length): **Fix** (option B — initialize): **Key lesson**: Nil slices are valid for `len`, `range`, `append`, but not for indexing. The error message is "index out of range", not "nil pointer dereference".Bug 7 (Medium) — Nil Function Variable Called¶
package main
import "fmt"
type Server struct {
onStart func()
}
func (s *Server) Start() {
fmt.Println("starting")
s.onStart()
}
func main() {
s := &Server{}
s.Start()
}
Solution
**Bug**: `s.onStart` was never assigned. Its value is `nil`. Calling a nil function value panics with the same message as nil pointer deref: (Internally, the call loads the funcval's code pointer, which is reading from address 0 — same fault.) **Fix** (option A — guard): **Fix** (option B — default in constructor): **Key lesson**: Calling a nil function variable panics. Set a no-op default or guard.Bug 8 (Medium) — Defer with Nil-Safe Wrapping¶
package main
import (
"errors"
"fmt"
)
type WrapErr struct {
inner error
op string
}
func (w *WrapErr) Error() string {
if w == nil {
return "<nil>"
}
return w.op + ": " + w.inner.Error()
}
func work() (err error) {
defer func() {
var w *WrapErr
if err != nil {
w = &WrapErr{op: "work", inner: err}
}
err = w
}()
return errors.New("boom")
}
func main() {
err := work()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("ok")
}
}
Solution
**Bug**: When `errors.New("boom")` is returned, the deferred function sets `w` to a real `&WrapErr{...}` and assigns it to `err`. That works fine. But — change the function to `return nil`: In this version, `work` returns nil but the caller's `err != nil` is true (typed nil interface). If `WrapErr.Error` were not nil-safe, calling `err.Error()` would panic. **Fix** — only assign `w` to err if it's actually non-nil: **Key lesson**: A deferred wrapper that always assigns its result back to `err` can introduce typed-nil bugs. Only wrap real errors; leave nil as bare nil.Bug 9 (Medium) — Pointer-Receiver Method NOT Nil-Safe¶
package main
import "fmt"
type List struct {
head *Node
n int
}
type Node struct {
val int
next *Node
}
func (l *List) First() int {
return l.head.val
}
func main() {
var l *List
fmt.Println(l.First())
}
Solution
**Bug**: `l` is nil. Calling `l.First()` is fine until the body executes. Then `l.head` requires reading the field through the nil receiver — panic. Even if `l` were non-nil but had `head == nil`, the body would dereference nil head and panic. **Fix**: **Key lesson**: Pointer-receiver methods that read fields are NOT nil-safe by default. Add the guard explicitly.Bug 10 (Hard) — Database Pointer Used Without Check¶
package main
import (
"database/sql"
"log"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("sqlite3", "file:test.db")
if err != nil {
log.Println(err)
return
}
}
func Query(id string) (string, error) {
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
return name, err
}
func main() {
name, err := Query("1")
if err != nil {
log.Println(err)
}
log.Println(name)
}
Solution
**Bug 1**: If `sql.Open` fails (rare but possible), `init` logs the error but does NOT panic and does NOT prevent later use of `db`. `db` becomes nil. `db.QueryRow` panics. **Bug 2**: Even when `sql.Open` "succeeds", it does not actually connect — the connection is lazy. So discovery of "wrong driver" / "bad DSN" happens later. But for THIS specific bug, the `db` pointer is non-nil even on misconfiguration; the panic moves to `db.Ping()` or query time with a real driver error. **Bug 3**: The `init` function is called automatically; if `sqlite3` driver is not registered (e.g., not imported), `sql.Open` returns `(nil, err)`. The if-block runs, prints the error, returns. `db` remains nil. Subsequent `db.QueryRow` panics. **Fix** (option A — fatal init):func init() {
var err error
db, err = sql.Open("sqlite3", "...")
if err != nil {
log.Fatal(err) // hard fail; process won't start
}
}
Bug 11 (Hard) — Slice of Pointers with Nil Entries¶
package main
import "fmt"
type Item struct {
Value int
}
func newItems(n int) []*Item {
items := make([]*Item, n)
for i := 0; i < n; i++ {
if i%3 == 0 {
items[i] = &Item{Value: i}
}
// else: leave nil
}
return items
}
func sum(items []*Item) int {
total := 0
for _, it := range items {
total += it.Value
}
return total
}
func main() {
items := newItems(10)
fmt.Println(sum(items))
}
Solution
**Bug**: `newItems` only populates 1 in every 3 slots; the rest are nil. `sum` iterates all and dereferences `it.Value` — panics on the first nil. **Fix** (option A — guard in sum):func sum(items []*Item) int {
total := 0
for _, it := range items {
if it == nil {
continue
}
total += it.Value
}
return total
}
Bug 12 (Hard) — Nil-Aware Wrapping with Sentinels¶
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type ContextErr struct {
cause error
op string
}
func (e *ContextErr) Error() string {
return fmt.Sprintf("%s: %v", e.op, e.cause)
}
func (e *ContextErr) Unwrap() error {
return e.cause
}
func fetch(id string) error {
var ctx *ContextErr
if id == "" {
ctx = &ContextErr{op: "fetch", cause: ErrNotFound}
}
return ctx
}
func main() {
err := fetch("123")
if err != nil {
fmt.Println("got error:", err)
} else {
fmt.Println("ok")
}
if errors.Is(err, ErrNotFound) {
fmt.Println("specifically not found")
}
}
Solution
**Bug**: `fetch("123")` does NOT enter the `if id == ""` branch, so `ctx` remains a nil `*ContextErr`. Returning it as `error` creates a typed-nil interface. `err != nil` is true. The "got error" path is taken even though no error occurred. `err` prints something like `Bonus Bug (Hard) — Pre-Initialized Map of Pointers, Sometimes Modified¶
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.RWMutex
items map[string]*Item
}
type Item struct {
Value int
}
func NewCache() *Cache {
return &Cache{items: map[string]*Item{}}
}
func (c *Cache) Get(k string) *Item {
c.mu.RLock()
defer c.mu.RUnlock()
return c.items[k]
}
func (c *Cache) Set(k string, v *Item) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[k] = v
}
func (c *Cache) Delete(k string) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[k] = nil
}
func main() {
c := NewCache()
c.Set("a", &Item{Value: 1})
c.Delete("a")
if v := c.Get("a"); v != nil {
fmt.Println(v.Value)
} else {
fmt.Println("absent")
}
fmt.Println("size:", len(c.items))
}