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
| Operator | Name | What it does | Example |
|---|---|---|---|
& | Address-of | Gets a pointer to a value | p := &x |
* | Dereference | Reads/writes the value at an address | fmt.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 changedThe * 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
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} - modifiedReason 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
| Scenario | Use value | Use pointer |
|---|---|---|
| Small struct (< 3-4 fields, no slices) | Yes | Rarely needed |
| Large struct or contains large arrays | No | Yes |
| Function needs to modify the argument | No | Yes |
| Struct has a mutex or other non-copyable field | No | Yes (must use pointer) |
| You want to express "this might be absent" (nullable) | No | Yes, nil means absent |
| Concurrent access patterns | Depends | Pointer + mutex |
*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?"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 dereferenceWith structs, this happens constantly:
type User struct {
Profile *Profile
}
type Profile struct {
Bio string
}
u := User{} // Profile is nil
fmt.Println(u.Profile.Bio) // panicuser.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
}
}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 type | Can modify struct? | Called on value? | Called on pointer? |
|---|---|---|---|
Value (c Counter) | No (copy) | Yes | Yes (auto-deref) |
Pointer (c *Counter) | Yes | Yes (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.
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{}.
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 WriterIf 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).
&X{} instead of X{}.When to use pointers: the short version
Use a pointer when:
- The function needs to modify the argument
- The struct is large and copying is expensive
- The type contains non-copyable fields (
sync.Mutex, etc.) - You need to represent absence (nil = not set)
- The type must satisfy an interface with pointer-receiver methods
Use a value when:
- The struct is small (a few scalar fields)
- 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
- 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)
*map[string]int is a pointer to a pointer, which is almost never what you want.