A slice []int is not the data. It is a tiny 16-byte handle — two fields, {data, len} — that views into a run of ints living somewhere else. Below, six ints sit in a backing array nums; each slice is just an address (where its view starts) plus a count (how many it covers). Pick a slice and watch which bytes of the handle change — and which ones don't.
Level 1 was what it is. Level 2 is why you'd reach for one: a slice is a cheap, mutable window. You hand a proc 16 bytes instead of copying the run — and because the handle points at the storage, a write through the slice lands on the backing array itself. This sharing-without-copying is called aliasing, and it's the whole reason slices exist.
Here are two slices over the same six-int backing array. mid = nums[1:4] sees indices 1–3; overlap = nums[2:5] sees 2–4. They share indices 2 and 3. Write 999 through one and watch where it shows up:
The mechanism: mid[1] is the same storage as nums[2], which is the same storage as overlap[0] — one location, three names. The write doesn't copy or "propagate"; it lands once, and every view reading that address sees it. That's why passing a []int into a proc lets the proc mutate the caller's data: the handle carries the address, so callee and caller are working the same bytes.
The counterfactual: if a proc only needs to read the run — sum it, print it — the aliasing is invisible and you still get the cheap 16-byte pass. You feel aliasing only when something writes. Reach for a slice when you want a write to stick in the caller's storage, or when copying the whole run would be wasteful. If you genuinely need an independent copy, that's a deliberate, separate act (slice.clone allocates fresh storage) — assignment alone never gives you one.
Level 2 sold you on the window. Level 3 is the bill: the handle has no idea whether its backing storage is still alive. A slice borrows a lifetime it doesn't own — and Odin has no garbage collector and no general lifetime tracking, so keeping the backing valid is your job.
The footgun is the dangling slice: a handle whose backing run has already been reclaimed. Watch a proc return a slice of its own local array:
Where Odin helps — and where it doesn't. Odin has a targeted escape check that rejects the most obvious shape at compile time: returning a slice of a local. That exact program does not build —
leak :: proc() -> []int { local: [4]int = {1, 2, 3, 4} return local[:] // a view into storage that's about to vanish } Error: It is unsafe to return a slice of a local variable ('local[:]') from a procedure, as it uses the current stack frame's memory
But that check guards one shape, not lifetimes in general. A slice that escapes indirectly — written through an out-parameter, tucked into a struct the caller keeps, taken into a growable buffer you then grow until it relocates, or into make-d storage you then delete — compiles silently and still dangles. The 16-byte handle doesn't flinch when its backing dies; the data pointer keeps pointing at the same address, which now holds whatever moved in next. Reading through it gives you stale values or a crash.
The last level is the payoff that emerges from "it's just an address + a count": because a slice owns no storage, you can carve a backing run into as many windows as you like, for free, and they all compose — while the half-open bounds stay machine-checked so a bad window is caught, not silently wrong.
Slicing is a[lo:hi], half-open: it includes lo, excludes hi, so len == hi - lo. And a slice of a slice is still a []int over the same backing — the bounds just compose by addition. From nums = {10,20,30,40,50,60}:
inner is a window of a window: tail = nums[3:] starts at index 3, then inner = tail[0:2] takes the first two of that — landing on nums[3:5]. Measured: raw_data(inner) == &nums[3] is true. Iterate it by reference (for &v in inner do v *= 2) and the doubling lands in the backing array: nums becomes [10, 20, 30, 80, 100, 60].
Each window costs nothing to make — no allocation, no copy, just a fresh 16-byte handle pointing into storage that already exists. The discipline that keeps this safe: the bounds are checked, and a bad one is a real error, not a quiet read of someone else's memory. Pick a way to break it:
The emergent payoff: a slice is so cheap and composable because it carries no storage — and that same fact is the bill from Level 3. One idea, two faces: "owns nothing" is what lets you mint a hundred zero-cost views over one buffer and pass each as a flat 16-byte handle; it's also why each view's life is tied to a backing run you must keep alive. The half-open bounds and the bounds check are what make slicing-everywhere safe to lean on: a window you can prove is in range, and one the runtime stops if you can't.
That's the arc: L1 a slice is a 16-byte {data, len} handle viewing storage elsewhere → L2 that handle aliases the backing, so a write through it sticks and a pass is just 16 bytes → L3 the bill is a borrowed lifetime the handle can't see (Odin guards the obvious dangle, nothing more) → L4 owning nothing is what makes windows free and composable, kept honest by checked half-open bounds.