Closures in Go

TL;DR Go implements closures by passing around a heap allocated struct which contains a function pointer and any closured variables.

 What are closures?

Closures, briefly, are functions that carry along with them variables declared within lexical scope of the defined function. So, what does this mean? Consider this:

package main

type generator func() int

func myFunc() generator {
        foo := 0
        return func() int {
                foo++
                return foo
         }
}

func main() {
        bar := myFunc()
        bar() // 1
        bar() // 2
}

The variable foo is in scope for the function that we declare and return from myFunc(). When we return our function, foo stays in scope, even though the function it was declared in has returned. How is this happening internally? To find out, lets look through some disassembled Go code!

Running go tool 6g -S foo.go provides us with this output (trimmed to relevant parts):

--- prog list "myFunc" ---
0000 (foo.go:5) TEXT    myFunc+0(SB),$24-8
0001 (foo.go:5) FUNCDATA $0,gcargs·0+0(SB)
0002 (foo.go:5) FUNCDATA $1,gclocals·0+0(SB)
0003 (foo.go:5) TYPE    ~anon0+0(FP){"".closure},$8
0004 (foo.go:5) TYPE    &foo+-8(SP){*int},$8
0005 (foo.go:6) MOVQ    $type.int+0(SB),(SP)
0006 (foo.go:6) PCDATA  $0,$16
0007 (foo.go:6) CALL    ,runtime.new+0(SB)
0008 (foo.go:6) PCDATA  $0,$-1
0009 (foo.go:6) MOVQ    8(SP),AX
0010 (foo.go:6) MOVQ    AX,&foo+-8(SP)
0011 (foo.go:6) MOVQ    $0,(AX)
0012 (foo.go:10) MOVQ    $type.struct { F uintptr; A0 *int }+0(SB),(SP)
0013 (foo.go:10) PCDATA  $0,$16
0014 (foo.go:10) CALL    ,runtime.new+0(SB)
0015 (foo.go:10) PCDATA  $0,$-1
0016 (foo.go:10) MOVQ    8(SP),AX
0017 (foo.go:10) NOP     ,
0018 (foo.go:10) MOVQ    $func·001+0(SB),BP
0019 (foo.go:10) MOVQ    BP,(AX)
0020 (foo.go:10) NOP     ,
0021 (foo.go:10) MOVQ    &foo+-8(SP),BP
0022 (foo.go:10) MOVQ    BP,8(AX)
0023 (foo.go:10) MOVQ    AX,~anon0+0(FP)
0024 (foo.go:10) RET     ,

--- prog list "func·001" ---
0047 (foo.go:7) TEXT    func·001+0(SB),$0-8
0048 (foo.go:7) FUNCDATA $0,gcargs·2+0(SB)
0049 (foo.go:7) FUNCDATA $1,gclocals·2+0(SB)
0050 (foo.go:7) TYPE    ~anon0+0(FP){int},$8
0051 (foo.go:7) MOVQ    8(DX),AX
0052 (foo.go:8) MOVQ    (AX),BP
0053 (foo.go:8) INCQ    ,BP
0054 (foo.go:8) MOVQ    BP,(AX)
0055 (foo.go:9) MOVQ    (AX),BP
0056 (foo.go:9) MOVQ    BP,~anon0+0(FP)
0057 (foo.go:9) RET     ,

The lines that we are most interested in currently are:

0005 (foo.go:6) MOVQ    $type.int+0(SB),(SP)
0006 (foo.go:6) PCDATA  $0,$16
0007 (foo.go:6) CALL    ,runtime.new+0(SB)

These lines are responsible for creating our integer on the heap. The PCDATA directive contains information for Go’s garbage collector, which is needed since we are dynamically allocating memory.

The next few lines of interest are:

0012 (foo.go:10) MOVQ    $type.struct { F uintptr; A0 *int }+0(SB),(SP)
0013 (foo.go:10) PCDATA  $0,$16
0014 (foo.go:10) CALL    ,runtime.new+0(SB)

These lines are creating a struct on the heap, with enough space for a function pointer and our closured int variable. The rest of the lines pack this newly allocated struct with a pointer to our function, and a pointer to our int and return.

0016 (foo.go:10) MOVQ    8(SP),AX
0018 (foo.go:10) MOVQ    $func·001+0(SB),BP
0019 (foo.go:10) MOVQ    BP,(AX)
0021 (foo.go:10) MOVQ    &foo+-8(SP),BP
0022 (foo.go:10) MOVQ    BP,8(AX)
0023 (foo.go:10) MOVQ    AX,~anon0+0(FP)
0024 (foo.go:10) RET     ,

With both pointers packed into the struct, we are able to call the function and still reference that variable we created. In the lines below, we call myFunc which returns our structure to us on the stack. We then move it from the stack into the DX register, and from there move our function into the BX register and call it.

0029 (foo.go:14) CALL    ,myFunc+0(SB)
0030 (foo.go:14) MOVQ    (SP),DX
0031 (foo.go:15) MOVQ    DX,bar+-8(SP)
0033 (foo.go:15) MOVQ    (DX),BX
0034 (foo.go:15) CALL    DX,BX

I originally wasn’t sure why CALL had 2 operands, until Russ Cox clarified it:

Now that we have moved our structure into DX, it is now available for our closured function. We move our int (which is 8 bytes offset from the memory location pointed to by the DX register) into AX, and then we move from AX to BP and increment. From there we return.

0051 (foo.go:7) MOVQ    8(DX),AX
0052 (foo.go:8) MOVQ    (AX),BP
0053 (foo.go:8) INCQ    ,BP

By allocating everything in heap memory, Go can safely pass around a pointer to our struct, which in turn contains pointers to our function and closured variable. Interesting, eh?

 
402
Kudos
 
402
Kudos