Skip to content
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

Closed
wolf0403 opened this issue Jul 28, 2016 · 34 comments
Closed

cmd/vet: warn about capturing loop iterator variables #16520

wolf0403 opened this issue Jul 28, 2016 · 34 comments
Labels
Analysis Issues related to static analysis (vet, x/tools/go/analysis) NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@wolf0403
Copy link

Please answer these questions before submitting your issue. Thanks!

  1. What version of Go are you using (go version)?
    go version go1.6 darwin/amd64
  2. What operating system and processor architecture are you using (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"
  3. What did you do?
    Run "go vet" on code from https://play.golang.org/p/mUiuNfZDHT
  4. What did you expect to see?
    Expect "go vet" warns about error mentioned in
    https://golang.org/doc/faq#closures_and_goroutines
  5. What did you see instead?
    No warning issued.
@quentinmit quentinmit added this to the Go1.8Maybe milestone Aug 1, 2016
@quentinmit quentinmit added the NeedsFix The path to resolution is known, but the work has not been done. label Oct 10, 2016
@rsc
Copy link
Contributor

rsc commented Oct 20, 2016

This does seem like it would catch real problems if done well.
/cc @adonovan

@rsc rsc modified the milestones: Go1.9, Go1.8Maybe Oct 20, 2016
@rsc
Copy link
Contributor

rsc commented Oct 20, 2016

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.

@alandonovan
Copy link
Contributor

alandonovan commented Oct 20, 2016

Although this bug pattern most often manifests with go statements (leading to data races), sequential (non-racy) instances are possible too, such as with defer or the simple example presented in this Issue.

vet already checks for patterns of the form:

for k, v := range seq {
    go/defer func() {
        ... k, v ...
    }()
}

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.

@mvdan
Copy link
Member

mvdan commented May 11, 2018

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?

@mvdan mvdan modified the milestones: Go1.11, Go1.12 May 11, 2018
@alandonovan
Copy link
Contributor

Does anyone even know how to solve this problem? I don't.

@gopherbot gopherbot modified the milestones: Go1.12, Unplanned May 23, 2018
@mvdan mvdan added NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. and removed NeedsFix The path to resolution is known, but the work has not been done. labels May 25, 2018
@mvdan
Copy link
Member

mvdan commented May 25, 2018

I'm labelling as NeedsInvestigation, just to clarify that we're not sure that a fix is possible.

@ianlancetaylor
Copy link
Contributor

See also #20733, which would eliminate this requirement by defining the problem away.

@gopherbot
Copy link

Change https://golang.org/cl/164119 mentions this issue: cmd/link: do not close over the loop variable in testDWARF

gopherbot pushed a commit that referenced this issue Feb 27, 2019
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>
@timothy-king
Copy link
Contributor

adds (*errgroup.Group).Go alongside defer and go as unsafe uses of function literals at the end of a loop

Is now supported in loopclosure.

improper t.Run and t.Parallel usage

This is something we have been considering adding support for in vet. This would most likely need to follow roughly the same pattern as loopclosure does. Someone from the community could contribute this.

We are currently in 1.18 code freeze, but this may be able to make it into 1.19.

@glenjamin
Copy link

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.

@timothy-king
Copy link
Contributor

@glenjamin Can you provide or point to a [possibly simplified] example? (This checker is quite sensitive to syntax to keep false positives down.)

@glenjamin
Copy link

@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
}

@timothy-king
Copy link
Contributor

@glenjamin That specific example is supportable in loopclosure. This would require extending the logic of looking for the "last" statement within the RangeStmt to include statements that unconditionally lead to the end of the RangeStmt if they are executed. If branches fit into this. Both branches do not need to match. Just one.

IMO the justification is fairly solid. We can rework the above example into something I expect loopclosure to warn on:

	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).

@porridge
Copy link

porridge commented Jan 5, 2022

Actually, note that those captures - at least when they are definite bugs - should usually show up in the race detector.

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:

$ go test -race -v .
=== RUN   TestParallel
=== RUN   TestParallel/case1
=== PAUSE TestParallel/case1
=== RUN   TestParallel/case2
=== PAUSE TestParallel/case2
=== CONT  TestParallel/case2
case2
=== CONT  TestParallel/case1
case2
--- PASS: TestParallel (0.00s)
    --- PASS: TestParallel/case2 (0.00s)
    --- PASS: TestParallel/case1 (0.00s)
PASS

@invidian
Copy link

invidian commented Jan 5, 2022

@porridge they would, if you run subtests in parallel.

@porridge
Copy link

porridge commented Jan 5, 2022

@invidian do they for you?
Adding -test.parallel=3 to that command line does not help on my end.

@invidian
Copy link

invidian commented Jan 5, 2022

Ah, I misread and haven't noticed t.Parallel(). Race detector does not trigger indeed. Sorry for the noise.

@porridge
Copy link

porridge commented Jan 5, 2022

We have a version of the loopclosure check within Monzo that uses a bunch of heuristics to avoid false positives and has good coverage of most bugs.

@danielchatfield any updates on making this available to the community?

@bcmills
Copy link
Contributor

bcmills commented Jan 5, 2022

@invidian, the t.Parallel issue in general is #35670. (It masks other kinds of races too, not just races on loop variables.)

@ianlancetaylor
Copy link
Contributor

See #56010 for a different approach.

thepudds added a commit to thepudds/xtools that referenced this issue Nov 18, 2022
…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
@gopherbot
Copy link

Change https://go.dev/cl/452155 mentions this issue: go/analysis/passes/loopclosure: recursively check last statements in statements like if, switch, and for

@thepudds
Copy link
Contributor

thepudds commented Nov 20, 2022

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).

@thepudds
Copy link
Contributor

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 i, node := i, node line:

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 }

@rsc
Copy link
Contributor

rsc commented Nov 21, 2022

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.

@golang golang deleted a comment from gopherbot Nov 21, 2022
gopherbot pushed a commit to golang/tools that referenced this issue Nov 21, 2022
…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>
@gopherbot
Copy link

Change https://go.dev/cl/455195 mentions this issue: go/analysis/passes/loopclosure: allow certain well-understood statements to trail problematic go and defer statements

@dmitris
Copy link
Contributor

dmitris commented Feb 29, 2024

Is this no longer relevant with go1.22 / should be closed? @adonovan

The long-standing “for” loop gotcha with accidental sharing of loop variables between iterations is now resolved.

@timothy-king
Copy link
Contributor

Yep. It is fine to close this with the 1.22 lifetime rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Analysis Issues related to static analysis (vet, x/tools/go/analysis) NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests