| --- | |
| title: Fixing For Loops in Go 1.22 | |
| date: 2023-09-19 | |
| by: | |
| - David Chase | |
| - Russ Cox | |
| summary: Go 1.21 shipped a preview of a change in Go 1.22 to make for loops less error-prone. | |
| template: true | |
| --- | |
| Go 1.21 includes a preview of a change to `for` loop scoping | |
| that we plan to ship in Go 1.22, | |
| removing one of the most common Go mistakes. | |
| ## The Problem | |
| If you’ve written any amount of Go code, you’ve probably made the mistake | |
| of keeping a reference to a loop variable past the end of its iteration, | |
| at which point it takes on a new value that you didn’t want. | |
| For example, consider this program: | |
| {{raw ` | |
| ~~~go | |
| func main() { | |
| done := make(chan bool) | |
| values := []string{"a", "b", "c"} | |
| for _, v := range values { | |
| go func() { | |
| fmt.Println(v) | |
| done <- true | |
| }() | |
| } | |
| // wait for all goroutines to complete before exiting | |
| for _ = range values { | |
| <-done | |
| } | |
| } | |
| ~~~ | |
| `}} | |
| The three created goroutines are all printing the same variable `v`, | |
| so they usually print “c”, “c”, “c”, instead of printing “a”, “b”, and “c” in some order. | |
| The [Go FAQ entry “What happens with closures running as goroutines?”](/doc/faq#closures_and_goroutines), | |
| gives this example and remarks | |
| “Some confusion may arise when using closures with concurrency.” | |
| Although concurrency is often involved, it need not be. | |
| This example has the same problem but no goroutines: | |
| {{raw ` | |
| ~~~go | |
| func main() { | |
| var prints []func() | |
| for i := 1; i <= 3; i++ { | |
| prints = append(prints, func() { fmt.Println(i) }) | |
| } | |
| for _, print := range prints { | |
| print() | |
| } | |
| } | |
| ~~~ | |
| `}} | |
| This kind of mistake has caused production problems at many companies, | |
| including a | |
| [publicly documented issue at Lets Encrypt](https://bugzilla.mozilla.org/show_bug.cgi?id=1619047). | |
| In that instance, the accidental capture of the loop variable was spread across | |
| multiple functions and much more difficult to notice: | |
| ```go | |
| // authz2ModelMapToPB converts a mapping of domain name to authz2Models into a | |
| // protobuf authorizations map | |
| func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) { | |
| resp := &sapb.Authorizations{} | |
| for k, v := range m { | |
| // Make a copy of k because it will be reassigned with each loop. | |
| kCopy := k | |
| authzPB, err := modelToAuthzPB(&v) | |
| if err != nil { | |
| return nil, err | |
| } | |
| resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{ | |
| Domain: &kCopy, | |
| Authz: authzPB, | |
| }) | |
| } | |
| return resp, nil | |
| } | |
| ``` | |
| The author of this code clearly understood the general problem, because they made a copy of `k`, | |
| but it turns out `modelToAuthzPB` used pointers to fields in `v` when constructing its result, | |
| so the loop also needed to make a copy of `v`. | |
| Tools have been written to identify these mistakes, but it is hard to analyze | |
| whether references to a variable outlive its iteration or not. | |
| These tools must choose between false negatives and false positives. | |
| The `loopclosure` analyzer used by `go vet` and `gopls` opts for false negatives, | |
| only reporting when it is sure there is a problem but missing others. | |
| Other checkers opt for false positives, accusing correct code of being incorrect. | |
| We ran an analysis of commits adding `x := x` lines in open-source Go code, | |
| expecting to find bug fixes. | |
| Instead we found many unnecessary lines being added, | |
| suggesting instead that popular checkers have significant false positive rates, | |
| but developers add the lines anyway to keep the checkers happy. | |
| One pair of examples we found was particularly illuminating: | |
| This diff was in one program: | |
| ```go | |
| for _, informer := range c.informerMap { | |
| + informer := informer | |
| go informer.Run(stopCh) | |
| } | |
| ``` | |
| And this diff was in another program: | |
| ```go | |
| for _, a := range alarms { | |
| + a := a | |
| go a.Monitor(b) | |
| } | |
| ``` | |
| One of these two diffs is a bug fix; the other is an unnecessary change. | |
| You can’t tell which is which unless you know more about the types | |
| and functions involved. | |
| ## The Fix | |
| For Go 1.22, we plan to change `for` loops to make these variables have | |
| per-iteration scope instead of per-loop scope. | |
| This change will fix the examples above, so that they are no longer buggy Go programs; | |
| it will end the production problems caused by such mistakes; | |
| and it will remove the need for imprecise tools that prompt users | |
| to make unnecessary changes to their code. | |
| To ensure backwards compatibility with existing code, the new semantics | |
| will only apply in packages contained in modules that declare `go 1.22` or | |
| later in their `go.mod` files. | |
| This per-module decision provides developer control of a gradual update | |
| to the new semantics throughout a codebase. | |
| It is also possible to use `//go:build` lines to control the decision on a | |
| per-file basis. | |
| Old code will continue to mean exactly what it means today: | |
| the fix only applies to new or updated code. | |
| This will give developers control over when the semantics change | |
| in a particular package. | |
| As a consequence of our [forward compatibility work](toolchain), | |
| Go 1.21 will not attempt to compile code that declares `go 1.22` or later. | |
| We included a special case with the same effect in | |
| the point releases Go 1.20.8 and Go 1.19.13, | |
| so when Go 1.22 is released, | |
| code written depending on the new semantics will never be compiled with | |
| the old semantics, unless people are using very old, [unsupported Go versions](/doc/devel/release#policy). | |
| ## Previewing The Fix | |
| Go 1.21 includes a preview of the scoping change. | |
| If you compile your code with `GOEXPERIMENT=loopvar` set in your environment, | |
| then the new semantics are applied to all loops | |
| (ignoring the `go.mod` `go` lines). | |
| For example, to check whether your tests still pass with the new loop semantics | |
| applied to your package and all your dependencies: | |
| GOEXPERIMENT=loopvar go test | |
| We patched our internal Go toolchain at Google to force this mode during all builds | |
| at the start of May 2023, and in the past four months | |
| we have had zero reports of any problems in production code. | |
| You can also try test programs to better understand the semantics | |
| on the Go playground by including a `// GOEXPERIMENT=loopvar` comment | |
| at the top of the program, like in [this program](/play/p/YchKkkA1ETH). | |
| (This comment only applies in the Go playground.) | |
| ## Fixing Buggy Tests | |
| Although we’ve had no production problems, | |
| to prepare for that switch, we did have to correct many buggy tests that were not | |
| testing what they thought they were, like this: | |
| ```go | |
| func TestAllEvenBuggy(t *testing.T) { | |
| testCases := []int{1, 2, 4, 6} | |
| for _, v := range testCases { | |
| t.Run("sub", func(t *testing.T) { | |
| t.Parallel() | |
| if v&1 != 0 { | |
| t.Fatal("odd v", v) | |
| } | |
| }) | |
| } | |
| } | |
| ``` | |
| In Go 1.21, this test passes because `t.Parallel` blocks each subtest | |
| until the entire loop has finished and then runs all the subtests | |
| in parallel. When the loop has finished, `v` is always 6, | |
| so the subtests all check that 6 is even, | |
| so the test passes. | |
| Of course, this test really should fail, because 1 is not even. | |
| Fixing for loops exposes this kind of buggy test. | |
| To help prepare for this kind of discovery, we improved the precision | |
| of the `loopclosure` analyzer in Go 1.21 so that it can identify and | |
| report this problem. | |
| You can see the report [in this program](/play/p/WkJkgXRXg0m) on the Go playground. | |
| If `go vet` is reporting this kind of problem in your own tests, | |
| fixing them will prepare you better for Go 1.22. | |
| If you run into other problems, | |
| [the FAQ](/wiki/LoopvarExperiment#my-test-fails-with-the-change-how-can-i-debug-it) | |
| has links to examples and details about using a tool we’ve written to identify | |
| which specific loop is causing a test failure when the new semantics are applied. | |
| ## More Information | |
| For more information about the change, see the | |
| [design document](https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md) | |
| and the | |
| [FAQ](/wiki/LoopvarExperiment). | |