Pointers Beginner¶
Introduction¶
A pointer holds the memory address of a value. Go uses pointers to enable mutation of shared data, avoid copying large structures, and express optionality (a pointer can be nil). Unlike C/C++, Go has no pointer arithmetic — pointers are safer and simpler while still giving you direct memory control.
Syntax & Usage¶
Declaration and Basic Operations¶
var x int = 42
var p *int = &x // p holds the address of x
fmt.Println(p) // 0xc0000b6010 (some memory address)
fmt.Println(*p) // 42 -- dereference: read the value at that address
*p = 100 // modify x through the pointer
fmt.Println(x) // 100
| Operator | Name | Purpose |
|---|---|---|
&x |
Address-of | Get the memory address of x |
*p |
Dereference | Read/write the value at address |
*T |
Pointer type | Declares a pointer to type T |
Zero Value of a Pointer Is nil¶
var p *int // nil -- points to nothing
fmt.Println(p) // <nil>
// fmt.Println(*p) // PANIC: runtime error: invalid memory address
if p != nil {
fmt.Println(*p)
}
The new() Function¶
new(T) allocates zeroed memory for type T and returns a *T.
new() vs composite literals
In practice, composite literals with & are far more common than new():
Pointers as Function Parameters (Mutation)¶
func increment(val int) {
val++ // modifies a copy -- caller's value unchanged
}
func incrementPtr(val *int) {
*val++ // modifies the original
}
x := 10
increment(x)
fmt.Println(x) // 10 -- unchanged
incrementPtr(&x)
fmt.Println(x) // 11 -- modified
Pointers to Structs¶
Go automatically dereferences struct pointers — you don't need (*p).Field.
type User struct {
Name string
Email string
}
u := &User{Name: "Alice", Email: "alice@example.com"}
fmt.Println(u.Name) // "Alice" -- no (*u).Name needed
u.Email = "new@example.com"
Pointer to Pointer¶
Rare in Go, but know it exists.
Pointer Receivers vs Value Receivers¶
type Counter struct {
n int
}
// Value receiver -- operates on a copy
func (c Counter) Value() int {
return c.n
}
// Pointer receiver -- can mutate the original
func (c *Counter) Increment() {
c.n++
}
c := Counter{}
c.Increment() // Go auto-takes address: (&c).Increment()
fmt.Println(c.Value()) // 1
Quick Reference¶
| Concept | Syntax | Notes |
|---|---|---|
| Declare pointer | var p *int |
Zero value is nil |
| Address-of | p = &x |
Get address of x |
| Dereference | *p |
Read/write value at address |
| Allocate | new(T) |
Returns *T, zeroed |
| Composite literal | &Config{...} |
Preferred over new() |
| Nil check | if p != nil |
Always check before dereferencing |
| Pointer receiver | func (s *S) M() |
Can mutate, can be nil |
| Value receiver | func (s S) M() |
Operates on copy |
| Pointer arithmetic | Not supported | Go is not C |
Best Practices¶
- Use pointers for mutation: If a function needs to modify the caller's data, pass a pointer.
- Use pointers for large structs: Avoids copying overhead for structs with many fields or large slices.
- Use pointers for optional/nilability: A
*stringcan benil, astringcannot — useful for distinguishing "not set" from empty. - Prefer value receivers when the method doesn't mutate: Makes the intent clear and is safe for concurrent use.
- Be consistent within a type: If any method needs a pointer receiver, use pointer receivers for all methods on that type.
- Don't return pointers to loop variables unless you understand capture semantics.
Common Pitfalls¶
Nil pointer dereference
The most common runtime panic in Go. Always check for nil before dereferencing.
Pointer to loop variable
Prior to Go 1.22, the loop variable was reused across iterations.
Returning a pointer to a local variable is fine in Go
Unlike C, Go's escape analysis moves the variable to the heap automatically.
Performance Considerations¶
| Scenario | Recommendation | Why |
|---|---|---|
| Small structs (1–3 fields) | Value | Copy is cheap, avoids heap allocation |
| Large structs (many fields) | Pointer | Avoids expensive copy |
| Slices, maps, channels | Value | Already reference types internally |
| Need mutation | Pointer | Only way to modify original |
| Hot path, high allocation rate | Value where possible | Reduces GC pressure |
Escape analysis
Use go build -gcflags="-m" to see what escapes to the heap. Pointer values that escape create GC work — keep hot-path data on the stack when possible.
Interview Tips¶
Interview Tip
"When should you use a pointer receiver vs a value receiver?" This is the #1 pointer question. Answer:
Use a pointer receiver when:
- The method needs to mutate the receiver
- The receiver is a large struct (avoids copy)
- Consistency — if any method needs a pointer receiver, use it for all
Use a value receiver when:
- The method is read-only and the struct is small
- You want the receiver to be safe for concurrent use without locks
- The type is a small value type like
time.Time
Interview Tip
"Does Go have pointer arithmetic?" No. This is a deliberate safety feature. You cannot increment a pointer to walk through memory like in C. This eliminates an entire class of buffer overflow vulnerabilities.
Interview Tip
"Is it safe to return a pointer to a local variable?" Yes. Go's escape analysis detects this and allocates the variable on the heap instead of the stack. The garbage collector manages its lifetime.
Key Takeaways¶
&gets an address,*dereferences it — the only two pointer operations you need.- Zero value of a pointer is
nil— always guard against nil dereference. - No pointer arithmetic — Go trades flexibility for safety.
- Use pointers for mutation, large structs, and nilability; use values for small immutable data.
- Prefer
&T{...}overnew(T)for readability. - Be consistent with receiver types across a type's method set.