Pointers & Memory Addressing
You have already used pointers without calling them that. Every append(&queue, …) you wrote passed an address, and every slice you created contained one. Here we slow down and look at what those symbols actually mean.
Why Pointers Exist
A pointer is not an advanced feature bolted onto a language. It is what memory is: a very long shelf of numbered bytes, where a value sits at some number. A pointer is simply that number, given a type so the compiler knows what is stored there.
An Address, Not a Value
Compare two variables. One holds data; the other holds the location of data:
count := 42 // count holds 42
address := &count // & means "the address of": address holds count's location
fmt.println(count) // 42
fmt.println(address) // &42 — the same value, seen through the address
Two different things, then, and the difference matters because an address can be copied, stored, and handed to another part of the program — while still referring to the one original value. That is the whole basis of sharing data without copying it.
The Two Honest Reasons
Pointers are not there to be clever with. They are there for exactly two needs:
- To let one part of the program change another part's data. A procedure receives copies of its parameters, as you learned in the procedures lesson; a pointer is how it reports a change back.
- To avoid copying something big. Copying a megabyte of vertex data into a procedure call would dwarf the work the procedure does.
If neither need applies, pass the value and enjoy not having to think about it. Most of the time that is the right answer, and Odin's design quietly encourages it.
Address-Of, Dereference, and the ^ Symbol
Pointers need three pieces of notation, and they all revolve around one character:
| Notation | Read it as | Meaning |
|---|---|---|
&x | "address of x" | Produces the location where x lives |
^T | "pointer to T" | The type of a value that holds an address of a T |
p^ | "the thing p points at" | Follows the address and gives you the value |
Taking an Address with &
The ampersand produces an address rather than a value. The result has a pointer type, written with a caret:
count := 42
p := &count // p is a ^int — a pointer to an int
fmt.println(p) // &42 — Odin shows the value through the pointer
fmt.printfln("%p", p) // 0x... — and %p shows the raw address itself
That second line is worth running once. Seeing the address printed as a plain number is the moment a pointer stops being an abstraction: it really is just a number naming a place in memory.
Following It with ^
The caret after a pointer name follows the address and hands you the value. Read p^ as "p's target":
count := 42
p := &count
fmt.println(p^) // 42 — the value at that address
// Because you now have the original, you can change it.
p^ = 7
fmt.println(count) // 7 — count itself changed
Notice the symmetry in the spelling: ^ in a type means "points to", and ^ after a name means "the thing pointed at". Both are the same thought seen from two sides.
Pointers to Structs Are Friendly
When a pointer aims at a struct, reaching a field needs no caret at all — the dot does the dereferencing for you:
Cat :: struct {
name: string,
age: int,
}
// The parameter is a pointer, so this procedure can change the caller's Cat.
birthday :: proc(cat: ^Cat) {
cat.age += 1 // no ^ needed before the dot
}
main :: proc() {
klucke := Cat{name = "Klucke", age = 5}
birthday(&klucke) // pass the address
fmt.println(klucke.age) // 6 — the original changed
}
That pattern — a struct passed as ^T — is all over Odin code, and now you know exactly what it buys: one address instead of a copy, plus the ability to report changes back. When a procedure should not modify the struct, take it by value and the question evaporates.
count is the data at that address, p is the address itself. & goes one way, ^ comes back.When to Reach for a Pointer
Three situations, and they cover almost every pointer you will write on purpose.
To Let a Procedure Change Your Data
This is the everyday case. A procedure receives copies of its parameters, so if it must change one, hand it the address instead:
// By pointer, so the caller's variable is the one that moves.
advance :: proc(position: ^int, steps: int) {
position^ += steps
}
main :: proc() {
place := 0
advance(&place, 3)
fmt.println(place) // 3 — the original changed
advance(&place, 4)
fmt.println(place) // 7
}
You saw this shape in the procedures lesson, and the same reasoning applies at every scale: append(&queue, …) and birthday(&klucke) are the same idea — the callee needs to write back, so the caller supplies a location.
To Avoid Copying Something Large
The second case is about cost. Structs are values, so passing one by value means the procedure gets its own copy — and for a big struct that is a lot of copying for nothing:
CHUNK :: 1920 * 1080 * 4 // about 8 MB of pixels
Frame :: struct {
pixels: [CHUNK]u8, // not something to copy casually
width: int,
height: int,
}
// One address instead of eight megabytes, and the real frame is modified.
draw :: proc(frame: ^Frame) {
frame.width = 1920
// ... fill frame.pixels ...
}
Ninety-nine percent of the time the compiler is clever enough that you would not notice the difference. Pass a pointer here anyway, because it documents the intent: this procedure works on your data rather than on a copy of it.
To Say "There Might Be Nothing"
A pointer has a natural empty state: an address pointing nowhere. Odin spells it nil, and for pointers nil is the sentinel value that marks "no object here":
current: ^Cat // the zero value of a pointer is nil
if current == nil {
fmt.println("no cat selected yet")
}
Two warnings, and they are the reason this is the third case rather than the first. A nil pointer is invisible in the type — nothing in ^Cat hints that it might be empty — and forgetting to check it is a classic crash. When "absent" is a genuine state of your data, prefer the union form you met in the structs lesson, which forces the check. Reach for a nil pointer when "no object" really is what you mean.
Raw Pointers and Raw Bytes
Two last tools, both used at the edges of the language rather than in the middle of your logic.
rawptr — The Untyped Address
rawptr is a pointer with no type attached: an address with no promise about what lives there. It exists because some code genuinely does not know or care — allocators, hashing, compression, and the foreign interface all work in bytes.
You will meet rawptr constantly in core:mem and in C bindings, and you will rarely need to write it yourself. When you do, the meaning is simply "here is a place in memory; the type is your problem".
Looking at the Bytes
When you need the address of the data behind a slice, string, or dynamic array, the built-in raw_data hands it over — with no type attached:
numbers := []int{1, 2, 3}
// The address of the underlying bytes, with no type attached.
bytes: rawptr = raw_data(numbers)
fmt.println(bytes) // the address, printed as a number
This is exactly what you hand to an API that treats data as bytes: hashing, compression, writing a buffer to a file, or a C function that takes a void*. The type information stays on your side of the call, which is where you still need it.
What Odin Does Not Do
Two deliberate omissions, and understanding them is what makes this lesson short rather than long.
No Pointer Arithmetic
In C you can add to an address and walk through memory: p + 1, p[3], p += 4. Odin removes all of it. A pointer aims at one thing, and walking over a sequence is what slices are for:
numbers := []int{10, 20, 30}
// Walk with an index; the slice already knows where the end is.
for i in 0..<len(numbers) {
fmt.println(numbers[i])
}
Once slices exist, pointer arithmetic has little left to offer except opportunities to be wrong about how many bytes an element occupies. And when you genuinely need to move by an offset — inside a binary parser, for instance — Odin gives you a computed answer rather than a pointer to walk:
Header :: struct {
tag: u8,
size: u32,
}
// offset_of asks the compiler where a member sits inside its struct.
fmt.println(offset_of(Header, size)) // 4 — bytes from the start of a Header
That is the honest replacement: work the offset out explicitly once, then index. If the number surprises you, the layout was not what you assumed — which is worth knowing before you write code that depends on it.
A Note on Multi-Pointers
Odin has a second pointer shape for something C expresses as "a pointer to a run of elements, length unknown": a multi-pointer, written [^]T. It is an address plus a count — a pointer that also carries a length, which is exactly the pair that makes a slice.
You meet it in low-level code, where a pointer and a size arrive separately and have to be put back together. The standard library does exactly that, and the one line is worth seeing because it shows the conversion:
// From the standard library: build a slice from a pointer plus a length.
// `raw_data` gives the address, `[^]byte` re-attaches a type, and [:n]
// attaches the count.
bytes := ([^]byte)(raw_data(s))[:len(s) * size_of(T)]
That is a multi-pointer being turned into a slice, and it is the pattern to recognise. For your own code, prefer the slice from the start — a slice is the same idea with the type visible and indexing bounds-checked, which is why this track has used slices for every "sequence of things" problem so far.
Pointers and Safety
Be clear about what Odin's pointers do and do not give you. They are manual: the compiler does not track how long the target lives, so a pointer to something that has been freed is still a bug you can write.
What Odin does give you is a language where the dangerous cases are visible rather than hidden. No pointer arithmetic to walk off the end. Bounds-checked indexing. Memory that starts at zero. A slice type that carries its length wherever it goes. The remaining risk is the honest one, and it is the price of speaking to memory directly.
Where This Goes Next
You now know how to talk about where memory is. The next lesson is about who provides it: the allocator, the context that carries it, and why Odin makes you choose rather than hiding the choice inside malloc.
The Page in One Breath
- A pointer is an address with a type:
^T.&xproduces one, andp^follows it back to the value. - Struct pointers let you use the dot directly —
cat.age— with no visible dereference. - Reach for a pointer to write back through a parameter, to avoid copying something large, or to mean "there might be nothing".
- A pointer's zero value is
nil— but nothing in the type warns you, so use a union when "absent" is real data. rawptris an address with no type, andraw_datagives you the bytes behind a slice, string, or dynamic array.- There is no pointer arithmetic: use slices to walk, and
offset_ofwhen you really need a byte offset. - Multi-pointers exist for foreign and low-level code; for your own sequences, a slice is the safer and clearer choice.
advance from this page, then write the same procedure taking int by value and watch the caller's variable refuse to change. Seeing both versions side by side is the fastest way to make the by-value rule permanent.
Continue with Allocators & the Context System →