Closures in Go are a very simple device, they happen when a function exists within another function, and that inner function has access to a variable declared in the outer function (the inner function is said to ‘close over’ the variable).
Whew, that’s a mouthful! Let’s have a look at a super simple example.
function Outer() {
var c string
func Inner() {
c = "I have been closed over"
}()
fmt.Println(c)
}
The Inner
function closes over the variable c
that is declared in the Outer
function.
If we stopped here, our understanding of closures would be superficial. What’s the point of having an inner function that closes over variables declared in outer functions.
With Go (in fact, most languages), if a called function returns data, then the data is stored somewhere that both functions can access - the heap.
The reason for this can be explained thus: When a function calls another function a stack of functions comes into existence A
calls B
so B
is executed, then any resources B
was using are released and A
can continue. Crucially the memory in the stack that B
was using is now available to be used by A
, as A
grows, or other functions that A
calls. This transience of B
‘owning’ the memory in the stack is why the data being shared needs to go out to the heap.
For the record, Go does escape analysis to determine if data is going to be stored in the stack, or out on the heap.
Let’s look at something else, Lifetimes in Rust.
The idea is that a Rust developer can tell the Rust compiler that data from a called function lives (and dies) within a specified scope.
This is where Closures (and the inlining of functions) comes into play for languages like Go.
By incorporating the called function into the same stack as the outer caller, the inner function’s memory is the outer functions. The closure allows the inner function to share data with the outer function without the need for the data to escape to the heap.
Note that this is only safe where both functions (and their shared data) are living on the same Goroutine
, as soon as the data/variable becomes shared across Goroutines then data races come into being, and the variable needs to be guarded (with synchronisation tools like mutexes).
Edit: In my haste to post I forgot one other option that is available. One of the reasons that the shared data is being sent to the heap is because the called function (B
) has no knowledge where the variable A
has put aside for receiving the output is. So, our third possible solution to preventing the data going out to the heap is for A
to give B
the address of the variable that A
wants B
to put the result in. Some care needs to be taken that the variable is at the end of the stack that A
‘owns’, and that there is enough room, because B
will be occupying the stack immediately after.
Summary
When a Go developer uses anonymous functions, they are telling the compiler the lifetime of the data inside them, and the closure allows the two functions to talk to one another without using the heap.