Pointers demystified. They're not scary — they're just addresses. Let's make them click.
In Python, you never think about this. In Go, you sometimes do. Here's the deal:
A pointer is just a variable that holds a memory address instead of a direct value.
// Visual representation
Regular variable:
x := 42
┌─────────┐
│ 42 │ ← x holds the value directly
└─────────┘
Pointer variable:
p := &x
┌─────────┐ ┌─────────┐
│ 0xc0001 │ ──→ │ 42 │ ← p holds address, points to value
└─────────┘ └─────────┘
p x
x := 42
// & = "address of" — get the memory address
p := &x // p is now a pointer to x
fmt.Println(p) // 0xc0000b4008 (some memory address)
// * = "dereference" — get the value at address
fmt.Println(*p) // 42 (the actual value)
// Modify through pointer
*p = 100
fmt.Println(x) // 100 — x changed!
Memory Trick:
&= "get Address" (& looks like 'A')
*= "get value" (go *****through the pointer)
Python has pointers too — you just can't see them. Everything in Python is a reference.
# Lists are mutable, passed by ref
def modify(lst):
lst.append(4)
my_list = [1, 2, 3]
modify(my_list)
print(my_list) # [1, 2, 3, 4] changed!
# Ints are immutable
def modify_int(x):
x = 100
n = 42
modify_int(n)
print(n) # 42 unchanged
// Slices work like Python lists
func modify(s []int) {
s[0] = 999 // Modifies original
}
// Ints: pass by value (copy)
func modifyInt(x int) {
x = 100 // Only changes copy
}
// Ints: pass by pointer (reference)
func modifyIntPtr(x *int) {
*x = 100 // Changes original!
}
n := 42
modifyIntPtr(&n) // Pass address
fmt.Println(n) // 100
The difference: Go makes you choose explicitly. Python hides the decision based on mutability.
nilint, bool, small structs — copying is fine// ❌ Unnecessary pointer — int is tiny
func double(x *int) int {
return *x * 2
}
// ✓ Just take the value
func double(x int) int {
return x * 2
}
// ✓ Pointer needed — modifying a struct
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// ✓ Pointer useful — large struct
type BigData struct {
Items [10000]int
}
func process(d *BigData) { /* ... */ }
A pointer that points to nothing is nil. It's Go's equivalent of Python's None for pointers.
var p *int // Declared but not initialized
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
// DANGER: Dereferencing nil crashes!
// fmt.Println(*p) // panic: runtime error
// Always check for nil
if p != nil {
fmt.Println(*p)
}
type Config struct {
Timeout *time.Duration // nil = use default
MaxSize *int // nil = unlimited
}
func NewServer(cfg Config) *Server {
timeout := 30 * time.Second // default
if cfg.Timeout != nil {
timeout = *cfg.Timeout
}
// ...
}
// Usage
customTimeout := 60 * time.Second
NewServer(Config{Timeout: &customTimeout})
NewServer(Config{}) // Uses defaults
nil Panic: Dereferencing a nil pointer causes a panic (crash). Always check
!= nilbefore using*p.
This is where pointers matter most in day-to-day Go.
type Counter struct {
count int
}
// Value receiver: gets a COPY
func (c Counter) IncrementBroken() {
c.count++ // Increments the copy, not original!
}
// Pointer receiver: gets the original
func (c *Counter) Increment() {
c.count++ // Actually increments
}
func main() {
c := Counter{}
c.IncrementBroken()
fmt.Println(c.count) // 0 — didn't work!
c.Increment()
fmt.Println(c.count) // 1 — works!
}
Go Does Some Magic: You can call pointer methods on values and vice versa -- Go automatically converts. But be consistent anyway.
// Method 1: & operator (most common)
x := 42
p := &x
// Method 2: new() — allocates zeroed memory, returns pointer
p := new(int) // *int pointing to 0
*p = 42
// Method 3: For structs, use & with literal
user := &User{Name: "Alice"} // Returns *User
// Equivalent to:
user := new(User)
user.Name = "Alice"
// new() — for any type, returns pointer to zero value
p := new(int) // *int → 0
s := new([]int) // *[]int → nil slice (not useful!)
// make() — ONLY for slices, maps, channels
// Returns initialized (not pointer!) value
slice := make([]int, 10) // []int with len=10
m := make(map[string]int) // Initialized map
ch := make(chan int, 5) // Buffered channel
Rule:
new()= zero value + pointer.make()= initialized slice/map/channel (no pointer).
func newUser() *User {
u := User{Name: "Alice"</span|}
return &u // Fine! Go moves u to heap
}
// In C this would be a bug. Go's escape analysis handles it.
users := []User{{Name: "A"}, {Name: "B"}, {Name: "C"}}
var ptrs []*User
// ❌ WRONG: All pointers point to same address!
for _, u := range users {
ptrs = append(ptrs, &u) // &u is same address each iteration
}
// All ptrs[i] point to "C"!
// ✓ CORRECT: Use index
for i := range users {
ptrs = append(ptrs, &users[i])
}
// ✓ OR in Go 1.22+: loop var is new each iteration
func process(u *User) {
// ❌ Crashes if u is nil
fmt.Println(u.Name)
// ✓ Check first
if u == nil {
return
}
fmt.Println(u.Name)
}
Progress through each section in order, or jump to where you need practice.
Practice individual concepts you just learned.
Combine concepts and learn patterns. Each challenge has multiple variants at different difficulties.
The Mantra: When in doubt, start without pointers. Add them when you need mutation or have large data.