A pointer is a value that holds the memory address of another value. Instead of “the data,” a pointer is “where the data lives.”
Two operators do all the work:
&xgives us the address ofx(a pointer to it).*pgives us the value that the pointerppoints at (we call this dereferencing).
x := 10
p := &x // p is a *int — a pointer to x
fmt.Println(*p) // 10 — follow the pointer to read the value
*p = 20 // write through the pointer
fmt.Println(x) // 20 — x changed because p pointed at it
Think of & as “give me the address of” and * as “go to that address.”
No pointer arithmetic
If we’ve used C, forget one habit. Go has no pointer arithmetic. We cannot do p++ to walk through memory. We can only point at something, read it, or write it.
Why? Safety. Pointer arithmetic is a huge source of bugs and security holes. Go’s garbage collector also needs to know exactly what’s a pointer, so it bans the math. This makes Go pointers much safer than C pointers.
Why pass pointers
Two real reasons we reach for pointers:
1. Mutation. Go passes everything by value (a copy). If a function should change the caller’s data, it needs a pointer.
func doubleCopy(n int) { n *= 2 } // changes a copy — useless
func doublePtr(n *int) { *n *= 2 } // changes the real value
v := 5
doubleCopy(v)
fmt.Println(v) // 5 — unchanged
doublePtr(&v)
fmt.Println(v) // 10 — changed
2. Avoid copying big structs. Passing a large struct by value copies every field. Passing a pointer copies just one address. Cheaper.
type BigConfig struct { /* ...dozens of fields... */ }
func process(c *BigConfig) { /* no expensive copy */ }
new vs &T{}
Two ways to allocate and get a pointer. They mostly do the same thing.
new(T)allocates a zeroedTand returns a*T.&T{...}makes a struct literal (optionally with field values) and returns its address.
type User struct{ Name string }
a := new(User) // *User, Name is "" (zero value)
b := &User{Name: "M"} // *User, Name is "M"
fmt.Println(a.Name, b.Name) // "" M
In practice we almost always use &T{...} because it lets us set fields right away. new is rare — handy mainly when we just want a zeroed pointer to a basic type. Note Go auto-dereferences for us: we write a.Name, not (*a).Name.
nil pointers
A pointer that points at nothing is nil. Reading a field through a nil pointer panics, so we guard against it.
var u *User // nil pointer
// fmt.Println(u.Name) // PANIC: nil pointer dereference
if u != nil {
fmt.Println(u.Name)
}
The classic “nil pointer dereference” panic is just us following a pointer that points nowhere. Always check before dereferencing if a pointer could be nil.
Interview soundbite
& takes an address, * follows it, and Go deliberately has no pointer arithmetic for safety and the garbage collector. We pass pointers for two reasons: to mutate the caller’s data (since Go copies by value) and to avoid copying large structs. We usually allocate with &T{...} over new, and we guard against nil pointers to avoid the dereference panic.