2023.11.16
Jalex Chang
Introduction
New language specification
Compatibility
Discussions
Summary
[1] Golang blog, https://go.dev/blog/loopvar-preview
[2] Proposal of Per-Iteration Loop Variable, https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md
[3] Official demo: https://github.com/golang/go/issues/60078#issuecomment-1671371277
[4] Related discussions: https://github.com/golang/go/discussions/56010
In this tech talk, we want to introduce the semantic changes on For Loop Variables since Go 1.22.
Topics will be covered in the talk:
Rationales behind the changes
New language specification
How Go handles compatibility for breaking changes
How does semantic change affect our daily life
var prints []func()
for i := 0; i < 3; i++ {
prints = append(prints, func() {
println(i)
})
}
for _, print := range prints {
print()
}
// Output:
// 3
// 3
// 3
var prints []func()
for _, s := range []string{"a", "b", "c"} {
prints = append(prints, func() {
println(s)
})
}
for _, print := range prints {
print()
}
// output:
// c
// c
// c
For Clause:
For Range:
Explicitly declare variables again within an iteration scope, forcing the variables to become per-iteration scoped.
var prints []func()
for i := 0; i < 3; i++ {
i := i
prints = append(prints, func() { println(i) })
}
for _, print := range prints {
print()
}
// Output:
// 0
// 1
// 2
var prints []func()
for _, s := range []string{"a", "b", "c"} {
s:=s
prints = append(prints, func() { println(s) })
}
for _, print := range prints {
print()
}
// output:
// a
// b
// c
For Clause:
For Range:
x:=x
".The init statement may be a short variable declaration (
:=
), but the post statement must not. Each iteration has its own separate declared variable (or variables). The variable used by the first iteration is declared by the init statement. The variable used by each subsequent iteration is declared implicitly before executing the post statement and initialized to the value of the previous iteration's variable at that moment.
var prints []func()
for i := 0; i < 3; i++ {
prints = append(prints, func() { println(i) })
}
var prints []func()
{
i_outer := 0
first := true
for {
i := i_outer
if first {
first = false
} else {
i++
}
if !(i < 3) {
break
}
prints = append(prints, func() { println(i) })
i_outer = i
}
}
The iteration variables may be declared by the “range” clause using a form of short variable declaration (:=). In this case their types are set to the types of the respective iteration values and their scope is the block of the “for” statement; each iteration has its own separate variables. If the iteration variables are declared outside the “for” statement, after execution their values will be those of the last iteration
var prints []func()
for _, s := range []string{"a", "b", "c"} {
prints = append(prints, func() { println(s) })
}
var prints []func()
{
var s_outer string
for _, s_outer = range []string{"a", "b", "c"} {
s := s_outer
prints = append(prints, func() { println(s) })
}
}
Go's official demo: https://go.dev/play/p/lDFLrPOcdz3
Enable the experiment flag "loopvar" to hint at Go compiler rewrites For Loop.
// GOEXPERIMENT=loopvar
package main
func main() {
var prints []func()
for i := range make([]int, 5) {
prints = append(prints, func() { println(i) })
}
for _, p := range prints {
p()
}
}
// output:
// 0
// 1
// 2
// 3
// 4
The change in language specification will fix far more programs than it breaks, but it may break a very small number of programs - buggy-already codes
To make the potential breakage completely user-controlled, the rollout would decide whether to use the new semantics based on the go
line in each package’s go.mod
file.
To transit the new semantics safely, two tools are supported:
go build (or test) gcflags=-d=loopvar=2 ...
bisect -compile=loopvar go test ...
We have used bisect in a conversion of Google's internal monorepo to the new loop semantics. The rate of test failure caused by the change was about 1 in 8,000.
$ go test ./loopvar/...
ok command-line-arguments 0.247s
// loopvar/sum_test.go
package main
import "testing"
func TestSum(t *testing.T) {
list := []int{2, 4, 6}
want := 12
if got := Sum(l); got != want {
t.Errorf("Sum(%v) = %v, want %v", list, got, want)
}
}
$ go install golang.org/dl/gotip@latest
$ gotip download
$ GOEXPERIMENT=loopvar gotip test ./loopvar/...
--- FAIL: TestSum (0.00s)
sum_test.go:9: Sum([2 4 6]) = 2, want 12
FAIL
FAIL loopvar 0.244s
FAIL
Old semantics:
New semantics:
// loopvar/sum.go
package main
func Sum(list []int) int {
m := make(map[*int]int)
for _, x := range list {
// In old semantic,
// value of &x is always the same.
m[&x] += x
}
for _, sum := range m {
return sum
}
return 0
}
func main() {
list := []int{2, 4, 6}
print(Sum(list))
}
$ gotip build -gcflags=-d=loopvar=2 ./loopvar
loopvar/sum.go:5:9: loop variable x \
now per-iteration, heap-allocated
$ GOEXPERIMENT=loopvar gotip test ./internal/...
ok github.com/chatbotgang/cantata/internal/adapter/eventbroker 1.299s
ok github.com/chatbotgang/cantata/internal/adapter/repository/es 1.267s
ok github.com/chatbotgang/cantata/internal/adapter/repository/gcs 1.506s
ok github.com/chatbotgang/cantata/internal/adapter/repository/local 1.793s
ok github.com/chatbotgang/cantata/internal/adapter/repository/postgres 2.402s
ok github.com/chatbotgang/cantata/internal/app/service/auth 1.317s
ok github.com/chatbotgang/cantata/internal/app/service/cdp 1.250s
ok github.com/chatbotgang/cantata/internal/app/service/chat 2.788s
ok github.com/chatbotgang/cantata/internal/app/service/organization 1.504s
......
ok github.com/chatbotgang/cantata/internal/app/service/utils 1.408s]
ok github.com/chatbotgang/cantata/internal/app/service/workertask 1.274s
ok github.com/chatbotgang/cantata/internal/domain/chat 1.299s
ok github.com/chatbotgang/cantata/internal/domain/common 1.449s
ok github.com/chatbotgang/cantata/internal/domain/common/requestid 1.668s
ok github.com/chatbotgang/cantata/internal/domain/organization 1.357s
ok github.com/chatbotgang/cantata/internal/router 1.305s
$ gotip build -gcflags=-d=loopvar=2 -o bin/cantata ./cmd/cantata
$
$ go install golang.org/x/tools/cmd/bisect@latest
$ bisect -compile=loopvar gotip test ./cmd/loopvar/...
bisect: checking target with all changes disabled
bisect: run: GOCOMPILEDEBUG=loopvarhash=n gotip test ./loopvar/...... ok (11 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=n gotip test ./loopvar/...... ok (11 matches)
bisect: checking target with all changes enabled
bisect: run: GOCOMPILEDEBUG=loopvarhash=y gotip test ./loopvar/...... FAIL (11 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=y gotip test ./loopvar/...... FAIL (11 matches)
bisect: target succeeds with no changes, fails with all changes
bisect: searching for minimal set of enabled changes causing failure
bisect: run: GOCOMPILEDEBUG=loopvarhash=+0 gotip test ./loopvar/...... FAIL (7 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=+0 gotip test ./loopvar/...... FAIL (7 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=+00 gotip test ./loopvar/...... FAIL (3 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=+00 gotip test ./loopvar/...... FAIL (3 matches)
......
bisect: run: GOCOMPILEDEBUG=loopvarhash=v+x0b0 gotip test ./loopvar/...... FAIL (1 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=v+x0b0 gotip test ./loopvar/...... FAIL (1 matches)
bisect: FOUND failing change set
--- change set #1 (enabling changes causes failure)
loopvar/sum.go:5:9: loop variable x now per-iteration
loopvar/sum.go:5:9: loop variable x now per-iteration (loop inlined into loopvar/sum.go:17)
loopvar/sum.go:5:9: loop variable x now per-iteration (loop inlined into loopvar/sum_test.go:8)
---
Use the same example as compiler flags (loopvar_test.go):
$ bisect -compile=loopvar gotip test ./internal/...
bisect: checking target with all changes disabled
bisect: run: GOCOMPILEDEBUG=loopvarhash=n gotip test ./internal/...... ok (102 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=n gotip test ./internal/...... ok (102 matches)
bisect: checking target with all changes enabled
bisect: run: GOCOMPILEDEBUG=loopvarhash=y gotip test ./internal/...... ok (102 matches)
bisect: run: GOCOMPILEDEBUG=loopvarhash=y gotip test ./internal/...... ok (102 matches)
bisect: fatal error: target succeeds with no changes and all changes
In this sharing, we have introduced the semantics changed on For Loop Variables since Go 1.22.
We are hiring now!