Memory
There is no garbage collector and no hidden allocation. A function that needs
the heap takes an Allocator:
import "std/mem"
func main() !int { mut backing := [1024]u8{} mut arena := mem.NewArena(backing[..])
mut xs := mem.Alloc[i64](&arena, 16) orelse return error.OutOfMemory for i in 0..xs.len { xs[i] = i64(i) * i64(i) }
return int(xs[8])}Arena bumps a pointer through a buffer it does not own — here, an array on
the stack. Individual releases do nothing; you throw the whole arena away at
once, or call Reset and start over. That is the intended shape: an arena per
phase, or per task, rather than tracking objects one at a time.
When you want the system allocator instead, mem.NewSystem() gives you one
over malloc that tracks what is still out:
import "std/mem"
func main() !int { mut sys := mem.NewSystem() mut xs := mem.Alloc[i64](&sys, 4) orelse return error.OutOfMemory xs[0] = 42 result := xs[0] mem.Free(&sys, xs)
if sys.Live() != 0 { return 1 } return int(result)}Both are values of type Allocator at the call site, so code written against
the interface works with either. Because an Allocator is a parameter and
never a global, a signature without one is a promise that the function does not
touch the heap — and the compiler has no way to break that promise.
Memory that has gone away
Section titled “Memory that has gone away”Two of the ways to read memory that is no longer yours are the compiler’s problem, and it takes them:
- A task cannot outlive the memory it borrowed.
scope’s closing brace joins, so lending a task a slice of your own stack is safe by construction. - A function cannot hand back its own frame.
return &local,return local[..],return &local[i]andreturn &local.fieldare compile errors, because the frame is gone before the caller can look.
func leak() []u64 { mut room := [4]u64{} return room[..] // error: memory that lives in this function's frame}The third way is yours: freeing something and then reading it. Nothing here tracks that — it would take the ownership machinery this language deliberately does not have — so what the standard library offers instead is a way to make the mistake loud instead of plausible.
mut arena := mem.NewArena(backing[..])arena.Watch = true // Reset writes 0xDE over everything it takes back
mut xs := mem.Alloc[u64](&arena, 2) orelse return error.OutOfMemoryxs[0] = 7arena.Reset()// xs[0] is now 0xDEDEDEDEDEDEDEDE rather than whatever came nextmem.NewWatched(&other) does the same for an allocator you release through one
piece at a time. Both cost a write per byte, which is why they are a choice rather
than the default — turn them on in a build you are debugging. shield test turns
Watch on for every test’s arena already, so a test is where this catches you.
Three habits make the difference in practice, and they are worth more than any checking:
- An arena per phase, and nothing crosses the reset. A server gives each request an arena and resets between requests; a response body read out of it is valid until that reset and not one line longer. If you need something to outlive the phase, copy it into memory that does.
- Borrowed memory is spelled out in the signature.
func (r *Request) Query(name string) stringhands back a piece of the connection’s read buffer, andstd/httpsays so where it is documented. When you write something similar, say it in the same place. - Free where you allocated. A function that takes an allocator and returns
memory from it leaves the freeing to its caller; a function that allocates for
its own working space frees it with
deferon the way out. Splitting those two roles between different functions is how a double free gets written.