There are 2 types of range, with index
and without index
. Let’s see an example for range with index
.
func TestRangeWithIndex(t *testing.T) {
rows := []struct{ index int }{{index: 0}, {index: 1}, {index: 2}}
for _, row := range rows {
row.index += 10
}
for i, row := range rows {
require.Equal(t, i+10, row.index)
}
}
the output is:
Error Trace: version_test.go:39
Error: Not equal:
expected: 10
actual : 0
Test: TestShowRangeWithIndex
Above test fails since when range with index
, the loop iterator variable is the same instance of the variable
with a clone of iteration target value
.
The same instance of the variable
Since the the loop iterator variable is the same instance of the variable
, it may result in hard to find error when use with goroutines.
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
for _ = range values {
<-done
}
You might mistakenly expect to see a, b, c as the output, but you’ll probably see instead is c, c, c. This is because each iteration of the loop uses the same instance of the variable v, so each closure shares that single variable.
This is the same reason which result wrong test when use t.Parallel()
with range.
func TestWithParallel(t *testing.T) {
tests := []struct {
name string
value int
}{
{name: "test 1", value: 4},
{name: "test 2", value: 4},
{name: "test 3", value: 4},
{name: "test 4", value: 4},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// uncomment below line will make 4 tests pass
//t.Parallel()
fmt.Printf("%+v == %+v \n", i+1, tc.value)
require.Equal(t, i+1, tc.value)
})
}
}
for above table driven tests, test1
, test2
, test3
should fail, test 4
should pass, it works as expect.
but if we add t.Parallel()
by uncomment the line, 4 test will pass, and the output is:
4 == 4
4 == 4
4 == 4
4 == 4
the reason is explained as above, here is another explain in Go common mistakes guide, 2 reason:
- In Go, the loop iterator variable is a single variable that takes different values in each loop iteration
- there is a very good chance that when you run this code you will see the last element printed for every iteration instead of each value in sequence, because the goroutines will probably not begin executing until after the loop
A clone of iteration target value
Since the loop iterator variable is a clone of iteration target value
, it may result in logic error if you do not pay attention. It can also lead to performance issue compared with without index
range or for
loop.
type Item struct {
id int
value [1024]byte
}
func BenchmarkRangeIndexStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for k := range items {
tmp = items[k].id
}
_ = tmp
}
}
func BenchmarkRangeStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
BenchmarkRangeIndexStruct-12 4875518 246.0 ns/op
BenchmarkRangeStruct-12 16171 77523 ns/op
You can see range with index
is much slower than range without index
, since range with index
use cloned value so have big performance decrease if iteration target
is a large struct which use a lot of memory.
this post is based on my research on a tidb maybe common mistakes with table driven tests and t.Parallel(), you can see more detail there.