You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
proposal: spec: redefine range loop variables in each iteration #20733
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.
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.
On Tue, 20 Jun 2017, 06:10 Brad Fitzpatrick ***@***.***> wrote:
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.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#20733 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAcA80GqcY1F4zKSo-optHwPwFOb1Jpks5sFtXGgaJpZM4N-uiT>
.
Reacted by Leo Balduf, Aleksandr Demakin, Bryan C. Mills, Justin Lowery, Peter Waller, Aliaksandr Valialkin, Xin.Zh, Christian Pena Valerio, Alexey Palazhchenko, Komu Wairagu and 50 more
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.
Reacted by Komu Wairagu, Oleg Kovalov, Андреев Кирилл, Kazuki, Eric Lagergren, bronze1man, Shaun Crampton, mikeweilgart, Akhil Indurti, Dmitri Shuralyov and 19 more
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).
Reacted by Jean-Francois SMIGIELSKI, Gizmich, Andrew W. Phillips and Muhammad Fathir Irhas
@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.
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.
Reacted by Peter Waller, Richard Wilkes, Eric Lagergren and Fumin
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.
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.
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)
}
Reacted by Vinícius Garcia, Alex Mills, Mark Pulford, Ingo Oeser, v1rtl, Artyom Andreev, Ilya Hrankoŭski, Carlo Alberto Ferraris, Alexey Palazhchenko, Quentin Quaadgras and 6 more
Reacted by Oleg Kovalov, v1rtl, Artyom Andreev, Andrii, Michael Bisgaard Olesen, Brian Conway, Teodor, Mark Pulford, Carlo Alberto Ferraris, Komu Wairagu and 8 more
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:
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.
Activity
bradfitz commentedon Jun 19, 2017
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.
davecheney commentedon Jun 19, 2017
dsnet commentedon Jun 19, 2017
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.
cznic commentedon Jun 20, 2017
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).
davecheney commentedon Jun 20, 2017
@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.
cznic commentedon Jun 20, 2017
@davecheney Consider #20725 (comment) and imagine escape analysis is not able to prove foo only reads via the passed pointer.
davecheney commentedon Jun 20, 2017
@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.
cznic commentedon Jun 20, 2017
@davecheney
Taking the address of a value does not imply intent to modify the pointee.
davecheney commentedon Jun 20, 2017
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.
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
Respectfully, generally it does, as in your method example above.
cznic commentedon Jun 20, 2017
Iff vals is a slice/array. There are also maps and channels.
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)'.
davecheney commentedon Jun 20, 2017
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.
I believe a copy of
valueis passed tofmt.Println. That copy is almost always heap allocated due to a limitation introduced in the Go 1.5 garbage collector.15 remaining items
cmd/link: do not close over the loop variable in testDWARF
bronze1man commentedon Oct 22, 2019
danielchatfield commentedon Jul 27, 2020
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:
candlerb commentedon Jan 2, 2021
Last year there was high profile example of this class of bug:
https://community.letsencrypt.org/t/2020-02-29-caa-rechecking-bug/114591
https://www.theregister.com/2020/03/03/lets_encrypt_cert_revocation/
Detailed analysis: https://bugzilla.mozilla.org/show_bug.cgi?id=1619047
Fix: https://github.com/letsencrypt/boulder/pull/4690/files#diff-d02067a9f9a2bed1110fd4e98641c2effcf5d1d5f18461e35d6ac1535f6e2c21L1411-R1414
earthboundkid commentedon Mar 16, 2022
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.XXit 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:to this as part of the upgrade:
{ i := 0 for ; i < n; i++ { use(&i) } }thepudds commentedon Mar 16, 2022
Hi @carlmjohnson, FWIW, the Go language changes design document uses this issue here as an example:
That document also describes using the
godirective 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 editwould be not used to change source code; it would more likely bego fixfrom what I understand.mpx commentedon Oct 4, 2022
FYI, there is a discussion covering how to redefine "for" loop variable semantics on #56010.
Merge #90221
rsc commentedon May 9, 2023
Closing as duplicate of #60078.