Skip to content

proposal: spec: redefine range loop variables in each iteration #20733

Closed

Description

@bisgardo

As an alternative to #20725 (which was originally about go vet), let the variables of range loops be implicitly redefined in each iteration like in Dart's for loops. That is,

for k, v := range vals {
  // ...
}

should be equivalent to

for k, v := range vals {
  k := k
  v := v
  // ...
}

This will make it "safe" to take the loop variable's addresses as well as capturing the loop variables in nested functions (see #16520).

The proposal could be expanded to vanilla for loops, although that would make it diverge semantically from other languages.

Activity

  1. added this to the Proposal milestone on Jun 19, 2017
  2. added
    v2An incompatible library change
    LanguageChangeSuggested changes to the Go language
    on Jun 19, 2017
  3. bradfitz commented on Jun 19, 2017

    @bradfitz
    Contributor

    We considered this prior to Go 1, but considered it too quickly. We'll definitely reconsider it for Go 2. I actually think there's a duplicate tracking bug for this.

  4. davecheney commented on Jun 19, 2017

    @davecheney
    Contributor
  5. dsnet commented on Jun 19, 2017

    @dsnet
    Member

    And I would argue even catches seasoned Go programmers occasionally. See https://golang.org/cl/40937. It took me a while staring at that logic trying to figure out why the code wasn't doing what it naturally read as.

  6. cznic commented on Jun 20, 2017

    @cznic
    Contributor

    Adopting this proposal can make performance of perfectly valid code worse, sometimes dramatically. For example, when escape analysis cannot determine it's safe to keep the {k,v} value on stack while it's address is taken. Then we get O(n) heap allocations instead of the current O(1).

  7. davecheney commented on Jun 20, 2017

    @davecheney
    Contributor

    @cznic can you give some examples of this. I would expect that most of these cases would actually be bugs because taking the address of the same variable over and over again was the problem in the first place.

  8. cznic commented on Jun 20, 2017

    @cznic
    Contributor

    @davecheney Consider #20725 (comment) and imagine escape analysis is not able to prove foo only reads via the passed pointer.

  9. davecheney commented on Jun 20, 2017

    @davecheney
    Contributor

    @cznic

    Why would you want to take the address of a copy of the current range value? If i'm not mistaken, any modification to it would be lost.

  10. cznic commented on Jun 20, 2017

    @cznic
    Contributor

    @davecheney

    Why would you want to take the address of a copy of the current range value?

    • To avoid copying a big value when passing it as an argument.
    • To call a pointer receiver method on the value.

    If i'm not mistaken, any modification to it would be lost.

    Taking the address of a value does not imply intent to modify the pointee.

  11. davecheney commented on Jun 20, 2017

    @davecheney
    Contributor

    To avoid copying a big value when passing it as an argument.

    But the value is already copied as part of the range iteration. Having a separate value escape on every iteration is sub optimal, but hopefully you'll agree it's reasonable to trade a potential performance problem for a very common correctness problem.

    To call a pointer receiver method on the value.

    True, that is asking a lot of escape analysis and I really don't know the ways that escape analysis and the implicit taking of address interact. But I will note that your example explicitly took the address of the copy of the current index value, so if the intention was to pass a pointer to the value down to a function, you'd want to make sure you were passing a pointer to the original slice element, not a copy. In the code that I have read that is more commonly written as

    for i := range vals {
         foo(&vals[i])
    }
    

    Taking the address of a value does not imply intent to modify the pointee.

    Respectfully, generally it does, as in your method example above.

  12. cznic commented on Jun 20, 2017

    @cznic
    Contributor

    foo(&vals[i])

    Iff vals is a slice/array. There are also maps and channels.

    Respectfully, generally it does, as in your method example above.

    Even the innocent looking fmt.Println(value) always takes the address of 'value'. As does every function taking an interface (not only interface{}, any interface) argument. Every assignment to an interface variable takes the address of the RHS (if not already a pointer), etc.

    Edit: Added '(if not already a pointer)'.

  13. davecheney commented on Jun 20, 2017

    @davecheney
    Contributor

    Iff vals is a slice/array. There are also maps and channels.

    If it's an array, it's already copied during the evaluation of the range statement. If it's a channel, it's a copy of the value received. Again i'm falling back on arguments of correctness over performance.

    Even the innocent looking fmt.Println(value) always takes the address of 'value'.

    I believe a copy of value is passed to fmt.Println. That copy is almost always heap allocated due to a limitation introduced in the Go 1.5 garbage collector.

  14. 15 remaining items

  15. removed
    NeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.
    on Aug 16, 2019
  16. added
    NeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.
    on Sep 3, 2019
  17. bronze1man commented on Oct 22, 2019

    @bronze1man
  18. danielchatfield commented on Jul 27, 2020

    @danielchatfield

    I'm very supportive of this change. For code that really needs to be optimised then the allocation can be avoided by declaring the variables outside the loop like this:

    var (
    	i int
    	v string
    )
    for i, v = range []string{"foo", "bar"} {
    	println(i, v)
    }
    
  19. candlerb commented on Jan 2, 2021

    @candlerb
  20. earthboundkid commented on Mar 16, 2022

    @earthboundkid
    Contributor

    I think this can be done now by keeping it behind a go.mod version flag. When you run go mod edit version -go=1.XX it should scan for existing closure captures and convert them to safe equivalents as part of the upgrade. I think it's doable. E.g. it would change this:

    for i := 0; i < n; i++ {
      use(&i)
    }

    to this as part of the upgrade:

    { 
      i := 0
      for ; i < n; i++ {
        use(&i)
      }
    }
  21. thepudds commented on Mar 16, 2022

    @thepudds
    Member

    Hi @carlmjohnson, FWIW, the Go language changes design document uses this issue here as an example:

    I think the only feasible safe approach is to not permit language redefinitions.

    We are stuck with our current semantics. This doesn't mean we can't improve them. For example, for issue 20733, the range issue, we could change range loops so that taking the address of a range parameter, or referring to it from a function literal, is forbidden. This would not be a redefinition; it would be a removal. That approach might eliminate the bugs without the potential of breaking code unexpectedly.

    That document also describes using the go directive in go.mod to control the version of the language used by each module (with some additional discussion & refinement in #28221).

    So I suspect it is at least possible to remove this as a feature. One minor comment is I suspect go mod edit would be not used to change source code; it would more likely be go fix from what I understand.

  22. added
    AnalysisIssues related to static analysis (vet, x/tools/go/analysis)
    on Mar 21, 2022
  23. mpx commented on Oct 4, 2022

    @mpx
    Contributor

    FYI, there is a discussion covering how to redefine "for" loop variable semantics on #56010.

  24. added a commit that references this issue on Oct 19, 2022
  25. rsc commented on May 9, 2023

    @rsc
    Contributor

    Closing as duplicate of #60078.

  26. locked and limited conversation to collaborators on May 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AnalysisIssues related to static analysis (vet, x/tools/go/analysis)FrozenDueToAgeLanguageChangeSuggested changes to the Go languageNeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.Proposalv2An incompatible library change

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions