@@ -18,39 +18,60 @@ import (
18
18
19
19
const Doc = `check references to loop variables from within nested functions
20
20
21
- This analyzer checks for references to loop variables from within a function
22
- literal inside the loop body. It checks for patterns where access to a loop
23
- variable used in the loop body is known to escape the current loop iteration:
24
- 1. a call to go or defer as the last statement
25
- 2. a call to golang.org/x/sync/errgroup.Group.Go as the last statement
26
- 3. a call testing.T.Run where the subtest body invokes t.Parallel()
27
-
28
- In the case of (1) and (2), the analyzer only considers references in the
29
- last statement of the loop body, where "last" is defined recursively.
30
- For example, if the last statement in the loop body is a switch statement,
31
- then the analyzer examines the last statements in each of the switch cases.
32
- Or if the last statement is a nested loop, then it examines the last
33
- statement of that loop's body, and so on. The analyzer is not otherwise
34
- deep enough to understand the effects of subsequent statements
35
- that might render the reference benign.
36
-
37
- Two examples that are caught:
38
-
39
- for _, v := range s {
40
- go func() {
41
- println(v) // each closure shares the same instance of v
42
- }()
43
- }
44
-
45
- for i, v := range s {
46
- if i == 0 {
47
- go func() {
48
- println(v) // each closure shares the same instance of v
49
- }()
50
- } else {
51
- println(v)
52
- }
53
- }
21
+ This analyzer reports places where a function literal references the
22
+ iteration variable of an enclosing loop, and the loop calls the function
23
+ in such a way (e.g. with go or defer) that it may outlive the loop
24
+ iteration and possibly observe the wrong value of the variable.
25
+
26
+ In this example, all the deferred functions run after the loop has
27
+ completed, so all observe the final value of v.
28
+
29
+ for _, v := range list {
30
+ defer func() {
31
+ use(v) // incorrect
32
+ }()
33
+ }
34
+
35
+ One fix is to create a new variable for each iteration of the loop:
36
+
37
+ for _, v := range list {
38
+ v := v // new var per iteration
39
+ defer func() {
40
+ use(v) // ok
41
+ }()
42
+ }
43
+
44
+ The next example uses a go statement and has a similar problem.
45
+ In addition, it has a data race because the loop updates v
46
+ concurrent with the goroutines accessing it.
47
+
48
+ for _, v := range elem {
49
+ go func() {
50
+ use(v) // incorrect, and a data race
51
+ }()
52
+ }
53
+
54
+ A fix is the same as before. The checker also reports problems
55
+ in goroutines started by golang.org/x/sync/errgroup.Group.
56
+ A hard-to-spot variant of this form is common in parallel tests:
57
+
58
+ func Test(t *testing.T) {
59
+ for _, test := range tests {
60
+ t.Run(test.name, func(t *testing.T) {
61
+ t.Parallel()
62
+ use(test) // incorrect, and a data race
63
+ })
64
+ }
65
+ }
66
+
67
+ The t.Parallel() call causes the rest of the function to execute
68
+ concurrent with the loop.
69
+
70
+ The analyzer reports references only in the last statement,
71
+ as it is not deep enough to understand the effects of subsequent
72
+ statements that might render the reference benign.
73
+ ("Last statement" is defined recursively in compound
74
+ statements such as if, switch, and select.)
54
75
55
76
See: https://golang.org/doc/go_faq.html#closures_and_goroutines`
56
77
@@ -106,8 +127,13 @@ func run(pass *analysis.Pass) (interface{}, error) {
106
127
//
107
128
// For go, defer, and errgroup.Group.Go, we ignore all but the last
108
129
// statement, because it's hard to prove go isn't followed by wait, or
109
- // defer by return. "Last" is defined recursively, as described in the
110
- // documentation string at the top of this file.
130
+ // defer by return. "Last" is defined recursively.
131
+ //
132
+ // TODO: consider allowing the "last" go/defer/Go statement to be followed by
133
+ // N "trivial" statements, possibly under a recursive definition of "trivial"
134
+ // so that that checker could, for example, conclude that a go statement is
135
+ // followed by an if statement made of only trivial statements and trivial expressions,
136
+ // and hence the go statement could still be checked.
111
137
forEachLastStmt (body .List , func (last ast.Stmt ) {
112
138
var stmts []ast.Stmt
113
139
switch s := last .(type ) {
0 commit comments