29. Escape Analysis in Go

Returning a pointer to a local variable is legal in Go. As a C programmer, the following looks like an error to me, but it’s perfectly alright in Go.

func NewFile(fd int, name string) *File {
    f := File{fd, name, nil, 0}
    return &f

Above we have a function that defines the local variable f as a File struct, and then returns the address of that local variable. If this were C, the memory layout would look like this:

    The Stack

|      ...      |
|               |
|               |  \
|               |   } Stack frame of calling function
|               |  /
|               |  \
|  f : File     |   } Stack frame of NewFile
|               |  /

If this were C, when we return from NewFile, its stack frame disappears along with the f defined in it. But this isn’t C. To quote Effective Go,

Note that, unlike in C, it’s perfectly OK to return the address of a local variable; the storage associated with the variable survives after the function returns.

This begs the question, how? How could the storage of local variables survive function returns? The FAQ explains:

When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

In other words, the Go compiler may allocate all local variables, whose address is taken, on the garbage-collected heap (it actually did this until late 2011); however, it’s clever enough to recognize some cases when it’s safe to allocate them on the stack instead.

The code that does the “escape analysis” lives in src/cmd/gc/esc.c. Conceptually, it tries to determine if a local variable escapes the current scope; the only two cases where this happens are when a variable’s address is returned, and when its address is assigned to a variable in an outer scope. If a variable escapes, it has to be allocated on the heap; otherwise, it’s safe to put it on the stack.

Interestingly, this applies to new(T) allocations as well. If they don’t escape, they’ll end up being allocated on the stack. Here’s an example to clarify matters:

var intPointerGlobal *int = nil

func Foo() *int {
    anInt0 := 0
    anInt1 := new(int)

    anInt2 := 42
    intPointerGlobal = &anInt2

    anInt3 := 5

    return &anInt3

Above, anInt0 and anInt1 do not escape, so they are allocated on the stack; anInt2 and anInt3 escape, and are allocated on the heap.