Go/
Lesson

AI-generated Go code uses pointers everywhere, *Config, *http.Request, *sql.DB. Sometimes the pointer is necessary. Sometimes it's cargo-culted from examples. Your job is to evaluate whether a pointer is the right choice and to spot the nil-pointer traps that AI leaves behind.

Go pointers are simpler than C/C++: no pointer arithmetic, no manual memory management, no pointer-to-pointer gymnastics in most code. But they still require understanding.

The two operators

OperatorNameWhat it doesExample
&Address-ofGets a pointer to a valuep := &x
*DereferenceReads/writes the value at an addressfmt.Println(*p)
age := 25
ptr := &age        // ptr is *int, holds address of age
fmt.Println(*ptr)  // 25 - dereference to read
*ptr = 30          // write through the pointer
fmt.Println(age)   // 30 - age changed

The * symbol means two different things depending on context:

  • Next to a type: pointer type declaration, var p *int
  • Next to a variable: dereference, value := *p

02

Why pointers exist in Go

Go is pass-by-value for everything. When you call a function, every argument is copied. Pointers exist for two reasons:

Reason 1: mutation

// Without pointer - modifies a copy, caller sees nothing
func tryToReset(p Point) {
    p.X = 0
    p.Y = 0
}

// With pointer - modifies the original
func reset(p *Point) {
    p.X = 0  // Go auto-dereferences struct pointers
    p.Y = 0
}

pt := Point{X: 10, Y: 20}
tryToReset(pt)
fmt.Println(pt) // {10 20} - unchanged

reset(&pt)
fmt.Println(pt) // {0 0} - modified

Reason 2: avoiding expensive copies

type BigConfig struct {
    Data [10000]byte
    // ... many fields
}

// Copies 10KB+ every call
func processByValue(c BigConfig) { /* ... */ }

// Copies 8 bytes (one pointer)
func processByPointer(c *BigConfig) { /* ... */ }

The decision table

ScenarioUse valueUse pointer
Small struct (< 3-4 fields, no slices)YesRarely needed
Large struct or contains large arraysNoYes
Function needs to modify the argumentNoYes
Struct has a mutex or other non-copyable fieldNoYes (must use pointer)
You want to express "this might be absent" (nullable)NoYes, nil means absent
Concurrent access patternsDependsPointer + mutex
AI pitfall
AI tends to make everything a pointer, *string, *int, *bool, especially when generating code based on protobuf or ORM patterns. In plain Go, there's no reason to use *string unless you need to distinguish between "empty string" and "not set." Each unnecessary pointer adds a nil-check burden and a potential panic. When reviewing AI output, ask: "Does this need to be a pointer, or is the value type fine?"
03

Nil pointers: Go's most common runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary. panic

A nil pointer dereference crashes the program immediately. No recovery, no error return, just a stack traceWhat is stack trace?A list of function calls recorded at the moment an error occurs, showing exactly which functions were active and in what order..

var p *int
fmt.Println(p)  // <nil> - printing nil is safe
fmt.Println(*p) // panic: runtime error: nil pointer dereference

With structs, this happens constantly:

type User struct {
    Profile *Profile
}

type Profile struct {
    Bio string
}

u := User{} // Profile is nil
fmt.Println(u.Profile.Bio) // panic
AI pitfall
AI-generated code chains pointer accesses without nil checks: user.Profile.Settings.Theme. Any nil in that chain crashes the program. When you see a chain of dot-accesses through pointer fields, each one is a potential panic point. The fix is either nil checks at each level or ensuring constructors always initialize nested pointers.

Safe access patterns

// Guard clause
if u.Profile == nil {
    return ""
}
return u.Profile.Bio

// Constructor that prevents nil
func NewUser(name string) *User {
    return &User{
        Name:    name,
        Profile: &Profile{}, // never nil
    }
}
04

Pointer receivers vs value receivers

Methods on structs can take either a pointer or value receiver. This determines whether the method can modify the struct and whether it requires a pointer to call.

type Counter struct {
    n int
}

// Value receiver - gets a copy
func (c Counter) Value() int {
    return c.n
}

// Pointer receiver - can modify
func (c *Counter) Increment() {
    c.n++
}

The consistency rule

If any method on a type has a pointer receiver, all methods should use pointer receivers. Mixing creates confusion about whether calling a method copies the struct or not.

Receiver typeCan modify struct?Called on value?Called on pointer?
Value (c Counter)No (copy)YesYes (auto-deref)
Pointer (c *Counter)YesYes (auto address-of)Yes

Go auto-converts between value and pointer for method calls, so both work either way. But the semantics differ: value receivers always work on copies.

AI pitfall
AI mixes pointer and value receivers on the same type. This compiles and runs, but it signals inconsistency. More dangerously, if a value receiver method is called on a value (not a pointer), modifications inside the method are silently lost. Establish the rule: if the type has any mutation methods, all receivers should be pointers.
05

new() vs &T{}

Both allocate and return a pointer. &T{} is more common because it allows initialization:

// new() - returns *T with zero values
p := new(Point)     // *Point{X: 0, Y: 0}

// &T{} - returns *T with specified values
p := &Point{X: 1, Y: 2}

In practice, new() is rare in idiomatic Go. You'll almost always see &T{}.

06

Pointers and interfaces

An interface value can hold either a value or a pointer. But which one it holds affects method set compatibility:

type Writer interface {
    Write(data []byte) (int, error)
}

type FileWriter struct {
    path string
}

// Pointer receiver
func (f *FileWriter) Write(data []byte) (int, error) {
    // ...
}

var w Writer
// w = FileWriter{}    // compile error - FileWriter doesn't implement Writer
w = &FileWriter{}      // works - *FileWriter implements Writer

If a method is defined on *T, only *T satisfies the interface, not T. This is because Go can't reliably take the address of every value (some are not addressable).

AI pitfall
AI generates an interface implementation with pointer receivers, then tries to assign a value (not pointer) to the interface variable. The error message, "X does not implement Y (method has pointer receiver)", is clear but frequently confuses people reading AI output. The fix is to use &X{} instead of X{}.
07

When to use pointers: the short version

Use a pointer when:

  1. The function needs to modify the argument
  2. The struct is large and copying is expensive
  3. The type contains non-copyable fields (sync.Mutex, etc.)
  4. You need to represent absence (nil = not set)
  5. The type must satisfy an interface with pointer-receiver methods

Use a value when:

  1. The struct is small (a few scalar fields)
  2. You want immutabilityWhat is immutability?A coding practice where you never change existing data directly, but instead create a new copy with the changes applied.: the called function can't modify your data
  3. The value is a map, slice, channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue)., or function (these are already reference types internally)

Maps, slices, channels, and functions are reference types internally, they contain a pointer under the hood. Passing them by value already shares the underlying data. A *map[string]int is a pointer to a pointer, which is almost never what you want.