In Python, everything is a reference behind the scenes. In Go, you choose explicitly. A pointer holds a memory address instead of a value directly.
x := 42
// & = "address of" — get the memory address
p := &x // p is *int (pointer to int)
fmt.Println(p) // 0xc0000b4008 (some address)
// * = "dereference" — get the value at the address
fmt.Println(*p) // 42
// Modify through the pointer
*p = 100
fmt.Println(x) // 100 — x changed!
Memory trick:
&= "get Address" (&looks like A).*= "go through" the pointer to the value.
Go copies everything by default. When you pass a struct to a function, the function gets a copy. Changes to the copy don't affect the original.
// Pass by value — gets a COPY
func resetStatus(p Pod) {
p.Status = "Pending" // only changes the copy
}
pod := Pod{Name: "web-1", Status: "Running"}
resetStatus(pod)
fmt.Println(pod.Status) // still "Running"!
// Pass by pointer — gets the ADDRESS
func resetStatusPtr(p *Pod) {
p.Status = "Pending" // changes the original
}
resetStatusPtr(&pod)
fmt.Println(pod.Status) // "Pending"
# Python: mutable objects (lists, dicts, class instances) are always references
# Go: you choose. Value = safe copy. Pointer = shared, mutable.
Use a pointer when:
Use a value when:
// ✓ Pointer — modifies the struct
func (p *Pod) SetStatus(s string) { p.Status = s }
// ✓ Value — just reads fields, small struct
func (p Pod) FullName() string { return p.Namespace + "/" + p.Name }
// ✓ Pointer — large struct, avoid copying
func processMetrics(m *MetricsSnapshot) { /* ... */ }
// ✗ 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 }
A pointer that hasn't been assigned points to nothing — it's nil. This is Go's version of Python's None, but only for pointers, slices, maps, channels, interfaces, and functions.
var p *Pod // declared but not assigned
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
// DANGER: using a nil pointer crashes
// fmt.Println(p.Name) // panic: nil pointer dereference
// Always check first
if p != nil {
fmt.Println(p.Name)
}
Nil pointer dereferences are the most common crash in Go. In infra code, they usually happen when an API returns nil and you forget to check.
// Common infra pattern: API lookup that might return nothing
func FindPod(name string, pods []*Pod) *Pod {
for _, p := range pods {
if p.Name == name {
return p
}
}
return nil // not found
}
pod := FindPod("web-99", pods)
if pod == nil {
log.Fatal("pod not found")
}
fmt.Println(pod.Status) // safe — we checked
A common Go pattern is returning a pointer from a constructor — it signals that the caller gets a reference to the allocated value, and methods should use pointer receivers:
func NewPod(name, ns string) *Pod {
return &Pod{
Name: name,
Namespace: ns,
Status: "Pending",
Labels: make(map[string]string),
}
}
pod := NewPod("web-1", "production") // pod is *Pod
pod.SetStatus("Running") // pointer receiver
Slices, maps, and channels are already reference types internally. You don't need to pass
*[]Pod— just[]Podis fine. Appending inside a function won't grow the caller's slice though, so return the new slice if appending.