Go Pointers Basics — Junior Level¶
1. Introduction¶
What is it?¶
A pointer is a value that stores the memory address of another value. Instead of holding the data itself, a pointer says "the data is over there." Using a pointer, you can read or modify the original data — even from a different function.
How to use it?¶
x := 42 // x is an int
p := &x // p is a pointer to x; type *int
fmt.Println(*p) // 42 — dereference to read
*p = 99 // dereference to write
fmt.Println(x) // 99 — x has been modified
Three symbols to know: - *T — type "pointer to T" - &x — take address of x - *p — dereference pointer p
2. Prerequisites¶
- Variables and types
- Functions basics (2.6.1)
- Call by value (2.6.7)
3. Glossary¶
| Term | Definition |
|---|---|
| pointer | A value holding the address of another value |
| address | The memory location of a value |
| dereference | Access the value at a pointer's address (*p) |
| address-of | Get the address of a value (&x) |
| nil pointer | A pointer with no target; zero value of any pointer type |
| pointer type | *T for some type T |
| allocate | Reserve memory for a value (often with new or &T{}) |
| pointer arithmetic | Adding offsets to pointers — NOT allowed in Go |
4. Core Concepts¶
4.1 Declaring a Pointer¶
The zero value of any pointer type is nil.
4.2 Taking an Address¶
x := 5
p := &x
fmt.Println(p) // 0xc000010078 (some address)
fmt.Println(*p) // 5 — value at that address
4.3 Dereferencing¶
4.4 Nil Pointer¶
Always check for nil before dereferencing.
4.5 new Built-in¶
new(T) is roughly equivalent to &T{} for zero-initialization.
4.6 No Pointer Arithmetic¶
Unlike C, Go forbids pointer arithmetic. Use slices for indexed access.
4.7 Auto-Dereference for Field/Method Access¶
type Point struct{ X, Y int }
p := &Point{X: 1, Y: 2}
fmt.Println(p.X) // Go auto-dereferences: same as (*p).X
You don't need (*p).X (though it's also valid). Go inserts the dereference automatically.
5. Real-World Analogies¶
A house address vs the house: a pointer is like an address (e.g., "123 Main St"). The address is small and easy to copy. The house at that address is the actual data — large and immobile. Multiple addresses can refer to the same house.
A library book number vs the book: the catalog number (pointer) tells you where to find the book. You can copy the number, share it, lose it. The book itself is unique.
A phone number vs the person: a phone number lets you reach a person from anywhere. Many people can have your number — they all reach the same you.
6. Mental Models¶
Memory:
address 0x100: 5 ← x lives here
address 0x200: 0x100 ← p (a *int) holds the address of x
In Go syntax:
x : 5
&x : 0x100 (address of x)
*p : 5 (value at p's address)
p : 0x100 (the address itself)
Pointers are just typed memory addresses.
7. Pros & Cons¶
Pros¶
- Allow functions to mutate caller's variables
- Avoid copying large structs (pass pointer instead of value)
- Enable shared state (multiple pointers to same data)
- Enable linked data structures (linked lists, trees)
Cons¶
- Easy to forget nil checks → panics
- Aliasing (multiple pointers to same data) can cause subtle bugs
- Pointer indirection has small runtime cost (extra memory load)
- Sharing pointers across goroutines requires synchronization
8. Use Cases¶
- Mutating a caller's variable (
func incr(p *int)). - Passing large structs efficiently.
- Returning a "newly created" object (
func newUser() *User). - Optional values (nil indicates absence).
- Linked data structures.
- Method receivers when mutation is needed.
9. Code Examples¶
Example 1 — Basic¶
package main
import "fmt"
func main() {
x := 42
p := &x
fmt.Println("x:", x, "p:", p, "*p:", *p)
*p = 99
fmt.Println("x:", x)
}
Example 2 — Mutate via Pointer¶
package main
import "fmt"
func double(p *int) {
*p *= 2
}
func main() {
n := 5
double(&n)
fmt.Println(n) // 10
}
Example 3 — Nil Check¶
package main
import "fmt"
func safeRead(p *int) int {
if p == nil {
return 0
}
return *p
}
func main() {
fmt.Println(safeRead(nil)) // 0
n := 42
fmt.Println(safeRead(&n)) // 42
}
Example 4 — new Allocation¶
package main
import "fmt"
func main() {
p := new(int)
fmt.Println(*p) // 0
*p = 100
fmt.Println(*p) // 100
}
Example 5 — Pointer to Struct¶
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++
}
func main() {
u := &User{Name: "Ada", Age: 30}
birthday(u)
fmt.Println(u.Age) // 31
}
Example 6 — Returning a Pointer (Constructor Pattern)¶
package main
import "fmt"
type Counter struct{ N int }
func newCounter() *Counter {
return &Counter{N: 0}
}
func main() {
c := newCounter()
c.N++
c.N++
fmt.Println(c.N) // 2
}
Example 7 — Two Pointers to Same Variable¶
package main
import "fmt"
func main() {
x := 5
p1 := &x
p2 := &x
*p1 = 99
fmt.Println(*p2) // 99 — same x
fmt.Println(p1 == p2) // true
}
10. Coding Patterns¶
Pattern 1 — Mutator Function¶
Pattern 2 — Constructor Returning Pointer¶
Pattern 3 — Optional Value (Nil)¶
Pattern 4 — Helper Returning *Bool¶
func ptrTo[T any](v T) *T {
return &v
}
cfg := Settings{
Enabled: ptrTo(true), // for Optional[bool] semantics
}
11. Clean Code Guidelines¶
- Always check for nil before dereferencing pointers from external sources.
- Use pointers for mutation — don't pretend to mutate via value parameters.
- Document optional pointer fields — what does nil mean?
- Use constructors that return pointers consistently.
- Avoid pointer-to-pointer (
**T) unless necessary.
// Good
func (u *User) Rename(name string) { u.Name = name }
// Bad — claims to mutate but can't
func (u User) Rename(name string) { u.Name = name } // operates on copy
12. Product Use / Feature Example¶
A user manager with optional fields:
package main
import "fmt"
type Email struct{ Address string }
type User struct {
Name string
Email *Email // optional: nil means "no email"
}
func describe(u *User) {
fmt.Printf("Name: %s\n", u.Name)
if u.Email != nil {
fmt.Printf("Email: %s\n", u.Email.Address)
} else {
fmt.Println("Email: (none)")
}
}
func main() {
u1 := &User{Name: "Ada", Email: &Email{Address: "ada@example.com"}}
u2 := &User{Name: "Bob"}
describe(u1)
describe(u2)
}
*Email allows nil to represent "absent". For required fields, use the value type directly.
13. Error Handling¶
When a function takes pointers, validate them:
For pointer return values, callers should check:
14. Security Considerations¶
- Nil checks at API boundaries — caller-provided pointers may be nil.
- Be careful with shared pointers across goroutines — synchronize access.
- Don't return pointers to internal mutable state unless callers should be able to modify it.
- Defensive copy when storing caller-provided pointers' targets.
// Avoid: caller can mutate internal state
func (s *Service) Config() *Config { return s.config }
// Better: return a copy
func (s *Service) Config() Config { return *s.config }
15. Performance Tips¶
- Pointers are 8 bytes on 64-bit systems — passing them is essentially free.
- Pointer indirection has a small cost (~1-2 cycles) per dereference.
- Pointers force heap allocation when they escape — stack is faster.
- For small types, value pass is faster than pointer pass.
- For large types, pointer pass avoids the copy.
16. Metrics & Analytics¶
type Metric struct {
Name string
Value float64
}
func record(m *Metric) {
if m == nil { return }
fmt.Printf("[%s] %f\n", m.Name, m.Value)
}
17. Best Practices¶
- Use pointers for mutation; values for reading.
- For large types, prefer pointer parameters.
- Always nil-check at boundaries.
- Use
&T{...}to allocate and initialize together. - Use
new(T)for zero-initialized. - Avoid
**Tunless necessary. - Document what nil means for optional pointer fields.
18. Edge Cases & Pitfalls¶
Pitfall 1 — Nil Dereference Panic¶
Pitfall 2 — Pointer to Loop Variable (Pre 1.22)¶
var ptrs []*int
for _, x := range []int{1, 2, 3} {
ptrs = append(ptrs, &x) // pre-1.22: all same pointer!
}
x := x or upgrade to Go 1.22+. Pitfall 3 — Address of Map Value¶
Map values are not addressable.Pitfall 4 — Pointer Arithmetic¶
Use slices and indices instead.Pitfall 5 — Returning Pointer to Local¶
19. Common Mistakes¶
| Mistake | Fix |
|---|---|
Forgetting & to take address | Add & |
Forgetting * to dereference | Add * |
| Dereferencing nil | Check for nil first |
| Pointer arithmetic | Use slices |
| Modifying parameter expecting caller change | Use pointer |
20. Common Misconceptions¶
Misconception 1: "Pointers are always faster than values." Truth: For small types, value pass is faster (registers, no indirection). For large types, pointers win.
Misconception 2: "I should use pointers everywhere to be safe." Truth: Pointers add indirection complexity and nil-check requirements. Use them only when needed.
Misconception 3: "Pointers to local variables are dangerous (like C)." Truth: Go's escape analysis automatically heap-allocates. Safe.
Misconception 4: "new(T) is different from &T{}." Truth: They're equivalent for zero-initialization. &T{} is more flexible (allows specifying field values).
Misconception 5: "Comparing pointers compares the values they point to." Truth: It compares addresses. Use *p1 == *p2 to compare pointed-to values.
21. Tricky Points¶
*has two meanings: type (*T) vs operator (*p).&only works on addressable values.- Auto-dereference works for
.fieldand method calls but not*p. - Pointer methods can be called on values:
t.Method()becomes(&t).Method()automatically (if t is addressable). - Pointers work with generics:
func f[T any](p *T).
22. Test¶
package main
import "testing"
func incr(p *int) {
*p++
}
func TestIncr(t *testing.T) {
n := 0
incr(&n)
if n != 1 {
t.Errorf("got %d, want 1", n)
}
}
func TestNilHandling(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic on nil dereference")
}
}()
var p *int
_ = *p // should panic
}
23. Tricky Questions¶
Q1: What does this print?
A:99. Modification through pointer affects the original. Q2: Will this compile?
A: No. Map values are not addressable.Q3: What does this print?
A:false true. Different addresses, same values. 24. Cheat Sheet¶
// Type
var p *int
// Take address
p = &x
// Dereference
v := *p
*p = 42
// Nil check
if p == nil { ... }
// Allocate
p := new(int) // zero-init
p := &MyStruct{...} // initialized
// Auto-deref
p := &User{}
p.Name = "Ada" // same as (*p).Name
// Compare
p1 == p2 // address comparison
25. Self-Assessment Checklist¶
- I can declare a pointer variable
- I can take an address with
& - I can dereference with
* - I check for nil before dereferencing
- I use
new(T)and&T{} - I know Go has no pointer arithmetic
- I understand auto-dereference for field/method access
- I use pointers for mutation in functions
26. Summary¶
A pointer is a value holding the address of another value. Use *T for the type, &x to take an address, *p to dereference. The zero value is nil; dereferencing nil panics. Go has no pointer arithmetic. Use pointers to mutate caller variables, share data, or avoid copying large structs. The compiler handles allocation automatically — pointers to locals that escape are heap-allocated; otherwise they may stay on the stack.
27. What You Can Build¶
- Mutator functions
- Constructors returning pointers
- Optional fields (nil = absent)
- Linked data structures (lists, trees)
- Object pools and caches
- Method receivers with pointer types
28. Further Reading¶
- Go Spec — Pointer types
- Effective Go — Pointers vs values
- Go Tour — Pointers
- Go Blog — Allocation efficiency in high-performance Go services
29. Related Topics¶
- 2.6.7 Call by Value
- 2.7.2 Pointers with Structs
- 2.7.3 With Maps & Slices
- 2.7.4 Memory Management
- Chapter 3 Methods (pointer receivers)
30. Diagrams & Visual Aids¶
Pointer mechanics¶
Variable x: Pointer p:
┌─────────┐ ┌──────────┐
│ value 5 │ ← addr 0x100 │ 0x100 │
└─────────┘ └──────────┘
↑
this is what p stores
*p = read/write the value at p's address (= 5)
&x = the address of x (= 0x100)