Variable Capture and Loops in Go vs. Swift

Over the last few months, I’ve had cause to switch between writing app code – largely in Swift – and server code, almost entirely in Go. While the two languages are pretty different, on occasion I’ll stumble across a similarity that seems like it can ease the transition or lessen the learning curve.

This is a story of how one of those similarities was subtly but deeply misleading, and introduced a major bug in a Go application.

Our story begins with anonymous functions, which exist in both Swift and Go. In the former, blocks can be used for a variety of purposes, including deferring execution of some code to a later time. As a very basic example, we might loop over a few integers, printing each of them after an incrementally larger delay:

for i in 1...5 {
    let delay = Int64(i) * Int64(NSEC_PER_SEC)
    let when = dispatch_time(DISPATCH_TIME_NOW, delay)
    dispatch_after(when, dispatch_get_main_queue()) {
        print("\(i)")
    }
}

This loop prints, over the course of five seconds, the integers one through five on standard output. Now let’s say we wanted to do the same in Go:

for i := 1; i <= 5; i++ {
    delay := time.Duration(i) * time.Second
    time.AfterFunc(delay, func() {
        fmt.Printf("%v\n", i)
    })
}

With an appropriate mechanism to keep the program running (such as an empty select{}), this code also produces its output over five seconds on standard output. However, this is where the subtle difference creeps in: instead of printing one through five, it prints the number six five times.

This is the point where, coming from Swift, I’m thoroughly confused. First off, why six? More importantly, why the same value each time, when the delayed function is clearly dispatched in a loop where i has several different values?1

The difference, it turns out, has to do with how variables are bound in loops, and how values are captured in anonymous functions. The Swift (and Objective-C) behavior – which I was most used to at the time of writing – was to bind i as a different immutable value in each loop iteration, then capture a reference to that value each time through.

Go, on the other hand, binds a single mutable value for the entire loop, then captures a reference to that single variable instead, only getting the value in question at the time the function is executed. This means that, since i was finished incrementing past the end of the loop by the time any of the five deferred functions were invoked, the value of i referenced inside the functions was 6 – the value at which the loop condition check fails and the loop breaks.

The “workaround” involves forcing Go to bind the value of i separately for each invocation of the deferred function. One way of doing this is to wrap the entire deferral in another function that’s invoked immediately:

for i := 1; i <= 5; i++ {
    func(n int) {
        delay := time.Duration(n) * time.Second
        time.AfterFunc(delay, func() {
            fmt.Printf("%v\n", n)
        })
    }(i)
}

In this solution, note the switch from i to n inside the outer function, and the use of i as the sole argument to that function. This way, i is evaluated at the moment of the outer function call, so the individual values of i through the loop are captured.

That’s a lot of conceptual overhead, though, and it introduces another level of indentation for the entire loop body – something that can get tedious for larger functions. We can achieve a similar effect with a more direct approach: simply make a new local variable inside the loop, then write the rest of the code as before.

for i := 1; i <= 5; i++ {
    n := i
    delay := time.Duration(n) * time.Second
    time.AfterFunc(delay, func() {
        fmt.Printf("%v\n", n)
    })
}

Again, we immediately switch from i to n, but this time without the bonus syntax of a function declaration and call – we simply declare n to be a new local variable with the current value of i. Since that’s a newly bound variable that gets captured by the deferred function, then, we get the expected result on standard output.

Interestingly enough, we can even “introduce” this bug in Swift code by using a C-like loop instead of the nicer for-in syntax:

for var i = 1; i <= 5; i++ {
    let delay = Int64(i) * Int64(NSEC_PER_SEC)
    let when = dispatch_time(DISPATCH_TIME_NOW, delay)
    dispatch_after(when, dispatch_get_main_queue()) {
        print("\(i)")
    }
}

Since this style explicitly uses a single mutable i for the entire loop, rather than binding a new i for each iteration, the “buggy” behavior – printing five sixes – occurs. Swift is even kind enough to make the mutability of i here more explicit, by requiring it be annotated var in the loop declaration.

The serious bug I mentioned before, by the way, was a near-perfect rehash of the Go example code above: at one point in a Web service, I enumerated devices in a group, sending each one a message after a certain delay. Without an extra local variable inside the loop, though, I noticed in testing that every message was going to the same device, leaving all the others out in the cold. Like above, an extra local variable to capture each device cleaned the problem up neatly.

The exact behavior in each language is probably something that’s old hat to experienced developers in that particular language, but switching between the two can introduce odd quirks. I still think Go is a great choice for server-side development, and obviously advocate Objective-C or Swift for your Mac and iOS needs – just be careful when moving from one environment to the other!

Note: Sincere thanks to Joe Groff, who pointed out that the variable capture semantics are the same between Swift and Go – it’s the loop variable binding that differs. An earlier version of this post erroneously claimed that variable capture functioned differently in the two languages. Thanks, Joe!

  1. Experienced Go developers have likely spotted the bug already – please bear with the rest of us as we work through it!