Go Nil Pointer Dereference — Tasks¶
Instructions¶
Each task includes a description, starter code, expected output, and an evaluation checklist. Practice writing nil-safe code, designing APIs that prevent nil panics, and recovering at the right boundaries.
Task 1 — Nil-Safe Counter¶
Difficulty: Beginner Topic: Methods on nil receivers
Description: Implement a Counter type with Get, Add, and String methods that all behave reasonably on a nil receiver. Get returns 0, Add returns a new counter starting from 0+x if the receiver is nil, and String returns "
Starter Code:
package main
import "fmt"
type Counter struct {
n int
}
func (c *Counter) Get() int {
// TODO
return 0
}
func (c *Counter) Add(x int) *Counter {
// TODO
return nil
}
func (c *Counter) String() string {
// TODO
return ""
}
func main() {
var c *Counter
fmt.Println(c.Get()) // 0
c2 := c.Add(5)
fmt.Println(c2.Get()) // 5
fmt.Println(c.String()) // <nil counter>
fmt.Println(c2.String()) // 5
}
Expected Output:
Evaluation Checklist: - [ ] Get handles nil receiver, returns 0 - [ ] Add handles nil receiver, returns a new non-nil counter - [ ] String handles nil, returns sentinel string - [ ] No panics for any method called on nil
Task 2 — Constructor That Cannot Return Nil-Without-Error¶
Difficulty: Beginner Topic: Constructor invariants
Description: Write NewUser(name string) (*User, error) such that on success, the returned pointer is guaranteed non-nil; on failure, the pointer is nil and the error is non-nil.
Starter Code:
package main
import (
"errors"
"fmt"
)
type User struct {
Name string
}
func NewUser(name string) (*User, error) {
// TODO
return nil, errors.New("not implemented")
}
func main() {
if u, err := NewUser(""); err != nil {
fmt.Println("err:", err)
} else {
fmt.Println("u:", u.Name)
}
if u, err := NewUser("alice"); err != nil {
fmt.Println("err:", err)
} else {
fmt.Println("u:", u.Name)
}
}
Expected Output:
Evaluation Checklist: - [ ] Empty name produces an error and nil pointer - [ ] Valid name produces a non-nil pointer and nil error - [ ] Function never returns both non-nil - [ ] Function never returns both nil
Task 3 — Avoid Typed Nil Error¶
Difficulty: Beginner Topic: Typed-nil-in-interface bug
Description: Refactor compute(x int) error so it never returns a typed nil. The current implementation returns a *ComputeErr that may be nil.
Starter Code:
package main
import "fmt"
type ComputeErr struct {
code int
msg string
}
func (e *ComputeErr) Error() string {
return fmt.Sprintf("[%d] %s", e.code, e.msg)
}
func compute(x int) error {
var e *ComputeErr
if x < 0 {
e = &ComputeErr{code: 1, msg: "negative"}
}
return e // BUG: returns typed nil for x >= 0
}
func main() {
err := compute(5)
if err == nil {
fmt.Println("no error")
} else {
fmt.Println("error:", err)
}
}
Expected Output (after fix):
Evaluation Checklist: - [ ] Returning bare nil for the success case - [ ] Returning the typed pointer only for the failure case - [ ] err == nil correctly identifies success
Task 4 — Safe Map of Pointers¶
Difficulty: Intermediate Topic: Map lookup with nil safety
Description: Implement Get(id string) (*User, bool) over a map[string]*User. Return (nil, false) for missing keys and (u, true) for present keys (asserting u != nil since the design forbids storing nil values).
Starter Code:
package main
import "fmt"
type User struct {
Name string
}
type Store struct {
users map[string]*User
}
func (s *Store) Get(id string) (*User, bool) {
// TODO
return nil, false
}
func (s *Store) Add(u *User) error {
// TODO: forbid nil and empty Name
return nil
}
func main() {
s := &Store{users: map[string]*User{}}
_ = s.Add(&User{Name: "alice"})
_ = s.Add(nil) // should error
if u, ok := s.Get("alice"); ok {
fmt.Println("found:", u.Name)
}
if _, ok := s.Get("bob"); !ok {
fmt.Println("missing")
}
}
Expected Output:
Evaluation Checklist: - [ ] Add(nil) returns an error - [ ] Add(&User{}) (empty name) returns an error - [ ] Get returns (nil, false) for missing - [ ] Get never returns (nil, true) - [ ] Stored pointers are never nil
Task 5 — Boundary Recovery for Goroutines¶
Difficulty: Intermediate Topic: Recover at goroutine boundary
Description: Implement safeGo(fn func()) that runs fn in a goroutine, recovering from any panic and logging it. The main goroutine should be unaffected.
Starter Code:
package main
import (
"fmt"
"sync"
)
func safeGo(fn func(), wg *sync.WaitGroup) {
// TODO
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
safeGo(func() {
var p *int
_ = *p
}, &wg)
safeGo(func() {
fmt.Println("hello from goroutine")
}, &wg)
wg.Wait()
fmt.Println("main done")
}
Expected Output (panic message may vary in format):
hello from goroutine
recovered: runtime error: invalid memory address or nil pointer dereference
main done
(Order of first two lines may swap.)
Evaluation Checklist: - [ ] Goroutine panic is recovered, not propagated - [ ] WaitGroup Done is always called (even on panic) - [ ] Other goroutine continues - [ ] Main goroutine completes normally
Task 6 — Detect Typed Nil with Reflection¶
Difficulty: Intermediate Topic: Reflection-based nil detection
Description: Implement IsNil(v any) bool that returns true if v is nil OR a typed nil (a non-nil interface wrapping a nil pointer/map/slice/chan/func/interface).
Starter Code:
package main
import (
"fmt"
"reflect"
)
func IsNil(v any) bool {
// TODO
return false
}
func main() {
var p *int
var i any = p
fmt.Println(i == nil) // false (typed nil)
fmt.Println(IsNil(i)) // should be true
fmt.Println(IsNil(nil)) // true
fmt.Println(IsNil(42)) // false
fmt.Println(IsNil("hi")) // false
var m map[string]int
fmt.Println(IsNil(m)) // true
}
Expected Output:
Evaluation Checklist: - [ ] Returns true for bare nil - [ ] Returns true for typed-nil pointer wrapped in interface - [ ] Returns true for nil map / slice / chan / func - [ ] Returns false for non-nil values - [ ] Uses reflect.Value.IsNil correctly per kind
Task 7 — Linked List with Nil-Safe Methods¶
Difficulty: Intermediate Topic: Recursive nil safety
Description: Implement a singly-linked list with Len, Append, String methods. All should be safe when called on a nil list.
Starter Code:
package main
import "fmt"
type Node struct {
Val int
Next *Node
}
type List struct {
Head *Node
n int
}
func (l *List) Len() int {
// TODO: nil-safe
return 0
}
func (l *List) Append(v int) *List {
// TODO: nil-safe; return new or modified list
return l
}
func (l *List) String() string {
// TODO: nil-safe
return ""
}
func main() {
var l *List
fmt.Println(l.Len()) // 0
fmt.Println(l.String()) // []
l = l.Append(1)
l = l.Append(2)
l = l.Append(3)
fmt.Println(l.Len()) // 3
fmt.Println(l.String()) // [1 2 3]
}
Expected Output:
Evaluation Checklist: - [ ] Len returns 0 on nil receiver - [ ] Append creates a new list when called on nil - [ ] String returns "[]" on nil - [ ] No panics
Task 8 — HTTP Recovery Middleware¶
Difficulty: Advanced Topic: HTTP boundary recovery
Description: Write middleware that wraps an http.Handler to recover from panics, log the stack, and return a 500 response.
Starter Code:
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"runtime/debug"
)
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: defer + recover; on panic, log debug.Stack() and 500
next.ServeHTTP(w, r)
})
}
type buggy struct{}
func (buggy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var p *int
fmt.Fprintln(w, *p)
}
func main() {
h := recoverMiddleware(buggy{})
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
h.ServeHTTP(rec, req)
fmt.Println("status:", rec.Code)
log.SetFlags(0)
_ = debug.Stack
}
Expected Output (status; log on stderr):
Evaluation Checklist: - [ ] Panic does not propagate to caller - [ ] Response status is 500 - [ ] Stack trace is logged - [ ] Non-panicking requests pass through normally
Task 9 — Recover and Categorize¶
Difficulty: Advanced Topic: Distinguishing nil panic from other panics
Description: Write categorize(fn func()) string that runs fn, recovers any panic, and returns one of: "ok", "nil-deref", or "other-panic" based on the panic value's type.
Starter Code:
package main
import (
"fmt"
"runtime"
)
func categorize(fn func()) string {
// TODO: defer + recover; check for *runtime.PanicNilError
fn()
return "ok"
}
func main() {
fmt.Println(categorize(func() {})) // ok
fmt.Println(categorize(func() {
var p *int
_ = *p
})) // nil-deref
fmt.Println(categorize(func() {
panic("custom")
})) // other-panic
_ = runtime.NumGoroutine
}
Expected Output:
Evaluation Checklist: - [ ] Returns "ok" when fn does not panic - [ ] Returns "nil-deref" for *runtime.PanicNilError - [ ] Returns "other-panic" for other panic kinds - [ ] Uses Go 1.21+ typed panic value
Task 10 — Defensive Filter¶
Difficulty: Advanced Topic: Slice-of-pointers cleanup
Description: Given []*Item, write FilterValid that returns a new slice with all nil entries removed.
Starter Code:
package main
import "fmt"
type Item struct {
Value int
}
func FilterValid(items []*Item) []*Item {
// TODO
return nil
}
func main() {
items := []*Item{
{Value: 1},
nil,
{Value: 2},
nil,
{Value: 3},
}
cleaned := FilterValid(items)
fmt.Println(len(cleaned)) // 3
for _, it := range cleaned {
fmt.Println(it.Value)
}
}
Expected Output:
Evaluation Checklist: - [ ] Removes all nil entries - [ ] Preserves order of non-nil - [ ] Returns a new slice (does not modify input) - [ ] Handles empty input (returns empty) - [ ] Handles all-nil input (returns empty)
Task 11 — Method Value on Nil¶
Difficulty: Advanced Topic: Method values and nil receivers
Description: Demonstrate the difference between calling a method on a nil pointer directly versus capturing a method value first. Write tests showing when each panics.
Starter Code:
package main
import "fmt"
type T struct {
v int
}
func (t *T) Read() int {
return t.v // touches field
}
func (t *T) Type() string {
return "T" // does not touch field
}
func main() {
var t *T
// Direct call:
defer func() {
if r := recover(); r != nil {
fmt.Println("direct Read panic:", r)
}
}()
// TODO: demonstrate
// 1. Direct call to t.Type() — should print "T"
// 2. Direct call to t.Read() — should panic
// 3. Method value m := t.Type; m() — should print "T"
// 4. Method value r := t.Read; r() — should panic
}
Expected Output:
(Recovery only catches one panic in the example above; structure your code to demonstrate all four cases.)
Evaluation Checklist: - [ ] Direct nil-safe method call works - [ ] Direct field-touching method call panics - [ ] Method value of nil-safe method works when called later - [ ] Method value of field-touching method panics when called later - [ ] Each demonstration is wrapped in its own recover
Task 12 — Build a Simple Optional Type¶
Difficulty: Advanced Topic: Avoiding *T for "optional" data
Description: Implement a generic Option[T] with methods Some(v T), None(), Get() (T, bool), OrElse(default T) T. Use it instead of *T in a small example.
Starter Code:
package main
import "fmt"
type Option[T any] struct {
value T
has bool
}
func Some[T any](v T) Option[T] {
// TODO
return Option[T]{}
}
func None[T any]() Option[T] {
// TODO
return Option[T]{}
}
func (o Option[T]) Get() (T, bool) {
// TODO
var zero T
return zero, false
}
func (o Option[T]) OrElse(def T) T {
// TODO
return def
}
func main() {
a := Some(42)
b := None[int]()
if v, ok := a.Get(); ok {
fmt.Println("a:", v)
}
if _, ok := b.Get(); !ok {
fmt.Println("b: none")
}
fmt.Println("b or 0:", b.OrElse(0))
}
Expected Output:
Evaluation Checklist: - [ ] Some(v) produces an Option that has a value - [ ] None[T]() produces an Option without a value - [ ] Get returns (value, true) or (zero, false) - [ ] OrElse returns the value or the default - [ ] No nil pointers involved
Bonus Task — Postmortem Analyzer¶
Difficulty: Advanced Topic: Stack trace parsing
Description: Given a multi-line panic output as a string, write extractPanicLine(s string) (string, error) that returns the file:line of the function that originated the panic (the frame just below runtime.sigpanic).
Starter Code:
package main
import (
"errors"
"fmt"
"strings"
)
const sample = `panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1234]
goroutine 1 [running]:
main.(*User).Greet(...)
/home/user/code/main.go:15 +0x12
main.main()
/home/user/code/main.go:25 +0x18
`
func extractPanicLine(s string) (string, error) {
// TODO: find the first frame after the panic header
return "", errors.New("not implemented")
}
func main() {
line, err := extractPanicLine(sample)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Println("first frame:", line)
_ = strings.SplitN
}
Expected Output:
Evaluation Checklist: - [ ] Finds the first frame's file:line after the goroutine header - [ ] Returns an error for malformed input - [ ] Handles multiple frames (finds the topmost) - [ ] Uses standard string parsing (no regex required)