Skip to content

Commit 1ba80ff

Browse files
committed
go/analysis/passes/loopclosure: add TODO for recursively defining "trivial" statements; update doc string
1 parent c62d9aa commit 1ba80ff

File tree

1 file changed

+61
-35
lines changed

1 file changed

+61
-35
lines changed

go/analysis/passes/loopclosure/loopclosure.go

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,39 +18,60 @@ import (
1818

1919
const Doc = `check references to loop variables from within nested functions
2020
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.)
5475
5576
See: https://golang.org/doc/go_faq.html#closures_and_goroutines`
5677

@@ -106,8 +127,13 @@ func run(pass *analysis.Pass) (interface{}, error) {
106127
//
107128
// For go, defer, and errgroup.Group.Go, we ignore all but the last
108129
// 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.
111137
forEachLastStmt(body.List, func(last ast.Stmt) {
112138
var stmts []ast.Stmt
113139
switch s := last.(type) {

0 commit comments

Comments
 (0)