-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cmd/vet: warn about capturing loop iterator variables #16520
Comments
This does seem like it would catch real problems if done well. |
Actually, note that those captures - at least when they are definite bugs - should usually show up in the race detector. It's not clear vet can contribute more than the race detector does without significant false positives. |
Although this bug pattern most often manifests with vet already checks for patterns of the form:
The example presented in this Issue is more challenging to analyze because it requires proving that the function is not called within the loop, or at least failing to prove that it is called within the loop. Once the anonymous function has been stored in a data structure or passed to another function, vet can no longer precisely determine when it might be called. In other words, I think the current vet check is probably as good as we can do without interprocedural analysis. |
It seems to me like we're late to add a new vet check in 1.11, given that the tree is frozen. Is anyone intending to work on this for 1.12? |
Does anyone even know how to solve this problem? I don't. |
I'm labelling as NeedsInvestigation, just to clarify that we're not sure that a fix is possible. |
See also #20733, which would eliminate this requirement by defining the problem away. |
Change https://golang.org/cl/164119 mentions this issue: |
Fixes #30429 Updates #16520 Updates #20733 Change-Id: Iae41f06c09aaaed500936f5496d90cefbe8293e4 Reviewed-on: https://go-review.googlesource.com/c/164119 Run-TryBot: Bryan C. Mills <bcmills@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Cherry Zhang <cherryyz@google.com> Reviewed-by: Ian Lance Taylor <iant@golang.org>
Fwiw the specific case that led me to this issue was that my loop contained an if statement as the last statement, and my goroutine(s) we're the last statements in each if branch. |
@glenjamin Can you provide or point to a [possibly simplified] example? (This checker is quite sensitive to syntax to keep false positives down.) |
@timothy-king sure - The body of the loop here is almost identical to my real code (I deleted a couple of lines of logging from the top of the loop body) This compiles and passes vet, but is incorrect. package main
import (
"context"
"sync"
"golang.org/x/sync/errgroup"
)
func main() {
var nodes []interface{}
ctx := context.Background()
critical, ctx := errgroup.WithContext(ctx)
others := sync.WaitGroup{}
for i, node := range nodes {
// i, node := i, node
if IsCritical(node) {
critical.Go(func() error {
return run(ctx, i, node)
})
} else {
others.Add(1)
go func() {
// We don't care about errors in non-critical nodes
_ = run(ctx, i, node)
others.Done()
}()
}
}
}
func IsCritical(node interface{}) bool {
return false
}
func run(ctx context.Context, i int, node interface{}) error {
return nil
} |
@glenjamin That specific example is supportable in IMO the justification is fairly solid. We can rework the above example into something I expect for i, node := range nodes {
// i, node := i, node
if IsCritical(node) {
critical.Go(func() error {
return run(ctx, i, node)
})
continue // minor refactor
}
others.Add(1)
go func() { // now the last statement
// We don't care about errors in non-critical nodes
_ = run(ctx, i, node)
others.Done()
}()
} Again we welcome community contributions on this (and we are currently in the 1.18 freeze). |
Unfortunately @rsc , they don't, in a typical buggy table test case: func TestParallel(t *testing.T) {
tests := []struct {name string}{{name: "case1"}, {name: "case2"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tt.name)
})
}
} This buggy output and no warning from race detector:
|
@porridge they would, if you run subtests in parallel. |
@invidian do they for you? |
Ah, I misread and haven't noticed |
@danielchatfield any updates on making this available to the community? |
See #56010 for a different approach. |
…statements like if, switch, and for In golang/go#16520, Allan Donovan suggested the current loopclosure check could be extended to check more statements. The current loopclosure flags patterns like: for k, v := range seq { go/defer func() { ... k, v ... }() } The motivating example for this CL from golang/go#16520 was: var wg sync.WaitGroup for i := 0; i < 10; i++ { for j := 0; j < 1; j++ { wg.Add(1) go func() { fmt.Printf("%d ", i) wg.Done() }() } } wg.Wait() The current loopclosure check does not flag this because of the inner for loop, and the checker looks only at the last statement in the outer loop body. Allan suggested we redefine "last" recursively. For example, if the last statement is an if, then we examine the last statements in both of its branches. Or if the last statement is a nested loop, then we examine the last statement of that loop's body, and so on. A few years ago, Allan sent a sketch in CL 184537. This CL attempts to complete Allan's sketch, as well as integrates with the ensuing changes from golang/go#55972 to check errgroup.Group.Go, which with this CL can now be recursively "last". Updates golang/go#16520 Updates golang/go#55972
Change https://go.dev/cl/452155 mentions this issue: |
Given the interest in possibly redefining for loop variable semantics in #56010, I thought it might be useful to try to complete the sketch for extending the loopclosure checker that @adonovan suggested above a few years ago in #16520 (comment). Rather than the checker examining the last statement in a loop body, the "last" statement examined by the checker becomes defined recursively (e.g., if the last statement in the loop body is a switch statement, then the analyzer examines the last statements in each of the switch cases). I sent CL https://go.dev/cl/452155, which does that, and now properly flags the example above from @dmowcomber. One question @timothy-king raised in the Gerrit discussion is whether or not that change would need to go through the proposal process. Are there any quick opinions on that? I'd be happy to open a proposal if that's the recommendation. (I will likely open a separate proposal for some possible follow-on approaches, but my more immediate question is if CL 452155 should have a proposal). |
Hi @glenjamin, FYI, https://go.dev/cl/452155 also seems to properly flag your example from above as well, and stops complaining if you uncomment the func main() {
var nodes []interface{}
ctx := context.Background()
critical, ctx := errgroup.WithContext(ctx)
others := sync.WaitGroup{}
for i, node := range nodes {
// i, node := i, node
if IsCritical(node) {
critical.Go(func() error {
return run(ctx, i, node) // vet now warns i and node captured by func literal
})
} else {
others.Add(1)
go func() {
// We don't care about errors in non-critical nodes
_ = run(ctx, i, node) // vet now warns i and node captured by func literal
others.Done()
}()
}
}
}
func IsCritical(node interface{}) bool { return false }
func run(ctx context.Context, i int, node interface{}) error { return nil } |
Given that this is improving the precision of an existing vet check, without introducing any new false positives, I don't believe it needs to go through the proposal process. |
…statements like if, switch, and for In golang/go#16520, there was a suggestion to extend the current loopclosure check to check more statements. The current loopclosure flags patterns like: for k, v := range seq { go/defer func() { ... k, v ... }() } For this CL, the motivating example from golang/go#16520 is: var wg sync.WaitGroup for i := 0; i < 10; i++ { for j := 0; j < 1; j++ { wg.Add(1) go func() { fmt.Printf("%d ", i) wg.Done() }() } } wg.Wait() The current loopclosure check does not flag this because of the inner for loop, and the checker looks only at the last statement in the outer loop body. The suggestion is we redefine "last" recursively. For example, if the last statement is an if, then we examine the last statements in both of its branches. Or if the last statement is a nested loop, then we examine the last statement of that loop's body, and so on. A few years ago, Alan Donovan sent a sketch in CL 184537. This CL attempts to complete Alan's sketch, as well as integrates with the ensuing changes from golang/go#55972 to check errgroup.Group.Go, which with this CL can now be recursively "last". Updates golang/go#16520 Updates golang/go#55972 Fixes golang/go#30649 Fixes golang/go#32876 Change-Id: If66c6707025c20f32a2a781f6d11c4901f15742a GitHub-Last-Rev: 04980e0 GitHub-Pull-Request: #415 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452155 Reviewed-by: Tim King <taking@google.com> Run-TryBot: Tim King <taking@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> Reviewed-by: Robert Findley <rfindley@google.com> Run-TryBot: Alan Donovan <adonovan@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
Change https://go.dev/cl/455195 mentions this issue: |
Is this no longer relevant with go1.22 / should be closed? @adonovan
|
Yep. It is fine to close this with the 1.22 lifetime rules. |
Please answer these questions before submitting your issue. Thanks!
go version
)?go version go1.6 darwin/amd64
go env
)?GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/ryangao/go"
GORACE=""
GOROOT="/Users/ryangao/homebrew/Cellar/go/1.6/libexec"
GOTOOLDIR="/Users/ryangao/homebrew/Cellar/go/1.6/libexec/pkg/tool/darwin_amd64"
GO15VENDOREXPERIMENT="1"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common"
CXX="clang++"
CGO_ENABLED="1"
Run "go vet" on code from https://play.golang.org/p/mUiuNfZDHT
Expect "go vet" warns about error mentioned in
https://golang.org/doc/faq#closures_and_goroutines
No warning issued.
The text was updated successfully, but these errors were encountered: