6.5 Why Go Has No Pointer Arithmetic
Right, so you’ve heard the horror stories. Pointer arithmetic in C is like giving a toddler a power drill: it’s incredibly effective for the one-in-a-million task it was designed for, and an absolute catastrophe for everything else. It’s the source of bugs so subtle and pernicious that they can take weeks to find, often only after your application has already mailed your entire customer database to a fax machine in Belarus.
Go’s designers, being people who have actually shipped software, took one look at this and said, “Nope.” They gave us the power of pointers—the ability to reference data directly rather than copying it willy-nilly—but they deliberately and surgically removed the ability to perform arithmetic on them. You can’t add to a pointer. You can’t subtract from it. You can’t compare two pointers from different allocations to see which is “greater.” This wasn’t an oversight; it was a design feature for safety and sanity.
The Safety Net You Didn’t Know You Wanted
Think about what pointer arithmetic allows you to do in C: you can take the address of an integer, add 1 to that address, and then pretend the result is a valid int. But what if it isn’t? You’ve just created a ticking time bomb. Go slams the door on this entire class of vulnerabilities. A *int is just that—a pointer to an integer. It is not the starting point for an array. It is not a cursor you can move around. It’s a single, specific address.
This design choice eliminates entire categories of bugs:
- Buffer overflows: You can’t accidentally (or deliberately) increment a pointer past the end of an array it points to.
- Invalid memory access: You can’t create a pointer to a random memory location by doing math on an existing one.
- Garbage memory: You can’t land in the middle of a struct and misinterpret the bytes there as a different type.
The compiler now has a much easier job guaranteeing memory safety. It can reason about what a pointer points to with far more confidence. This is a huge win for reliability, and frankly, it’s a trade-off I’ll make any day of the week.
But How Do I Iterate? (You Use a Slice, You Maniac)
I hear you. The immediate objection is, “Without pointer arithmetic, how do I walk through an array or a buffer?” And the answer is: you use a slice. A slice is this brilliant abstraction that gives you a pointer to an underlying array, a length, and a capacity. The pointer is fixed—you can’t change it with arithmetic—but the slice itself is a value you can reslice. You move your window of view over the data by changing the start index and length of the slice, not by changing the memory address itself.
This is safer because the slice header, not you, is responsible for tracking the bounds. Any access outside those bounds causes a runtime panic immediately, instead of silently corrupting memory. Let’s see it in action.
package main
import "fmt"
func main() {
data := []int{10, 20, 30, 40, 50}
slice := data[1:4] // This slice points to [20, 30, 40]
// This is safe, idiomatic iteration. The index is just an integer.
for i, v := range slice {
fmt.Printf("Index %d: %d\n", i, v)
}
// You can "advance" the slice by reslicing it.
// You're not moving a pointer; you're creating a new slice header that
// points to a different starting point in the same array.
advancedSlice := slice[1:]
fmt.Println(advancedSlice) // [30, 40]
// This would cause a runtime panic. The safety net works.
// fmt.Println(slice[5])
}
The One Weird Exception: unsafe.Pointer
Now, because Go is a practical language for systems programming, there is an escape hatch. The unsafe package gives you unsafe.Pointer, a special type that lets you convert any pointer type to any other pointer type. And yes, if you really need to, you can use it to perform de facto pointer arithmetic by converting your pointer to a uintptr, doing math on the integer, and converting it back.
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [3]int32{1, 2, 3}
ptr := &arr[0] // Get the address of the first element
// WARNING: DO NOT DO THIS UNLESS YOU HAVE A VERY GOOD REASON AND A WILL.
// Convert to unsafe.Pointer, then to uintptr to do math.
nextAddr := uintptr(unsafe.Pointer(ptr)) + unsafe.Sizeof(arr[0])
// Convert back to a pointer to int32
nextPtr := (*int32)(unsafe.Pointer(nextAddr))
fmt.Println(*nextPtr) // Prints 2
}
Look at that mess. It’s verbose, it’s ugly, and it’s called unsafe for a reason. You are manually turning off all the safety features the language provides. The Go compiler’s garbage collector does not understand uintptr arithmetic. If a GC cycle happened between you calculating nextAddr and converting it back, the entire array could be moved in memory, and you’d be left with a pointer to nowhere. This is for very specific, very rare scenarios—like interoperating with C libraries or writing super-low-level memory allocators. For 99.9% of your code, you should pretend unsafe doesn’t exist. The fact that it’s so cumbersome is a feature; it makes you question your life choices before using it.