Looping over a slice that changes
A for loop is often used to walk through a sequence such as a slice. Trouble starts when the loop changes the sequence it is walking through. Whether the loop ends then depends on how you wrote it.
The classic infinite loop
Here the loop condition asks how long the slice is, and the body makes it longer:
fruits := []string{"banana", "orange"}
for i := 0; i < len(fruits); i++ {
fruits = append(fruits, "kivi")
}
Each pass adds one element and increases i by one. The slice starts with two elements, so len(fruits) is 2 at the first check. After the first pass it is 3 while i is 1. After the second pass it is 4 while i is 2. The length is always one step ahead of i, so the condition i < len(fruits) never becomes false. The loop never reaches the end of the slice because the end keeps moving away. The program never stops, and in the end it would run out of memory.
If you want to see the pattern without hanging your program, add a guard that breaks out of the loop, and print the two values:
for i := 0; i < len(fruits); i++ {
fruits = append(fruits, "kivi")
fmt.Println(i, len(fruits))
if i == 3 {
break
}
}
This prints:
0 3
1 4
2 5
3 6
The length is always ahead of the index. The break is only there for the experiment. A real loop should end because its condition becomes false.
Range behaves differently
A for ... range loop does not recheck the length on every pass. Go looks at the slice once, when the loop starts, and walks over the elements that were there at that moment. If the body appends to the same slice, the loop does not see the new elements:
nums := []int{1, 2, 3}
for _, n := range nums {
nums = append(nums, n*10)
}
fmt.Println(nums)
This ends after three passes and prints:
[1 2 3 10 20 30]
So a range loop that appends to its own slice finishes. It is the form with i < len(...) that runs forever. Still, changing a slice while you walk through it is confusing to read, and it is usually better to collect new elements in a separate slice.
The exercise
The code should copy every name from newStudents to the end of students. The result must be Andy, Mary, Alex, Ana, Alexa and Mike. The code we start from is:
students := []string{"Andy", "Mary", "Alex"}
newStudents := []string{"Ana", "Alexa", "Mike"}
for _, student := range newStudents {
newStudents = append(newStudents, student)
}
The loop is the right shape: it visits each new student. The mistake is in the body. It appends to newStudents, the slice being walked through, so students never changes. The loop still ends after three passes, because of the way range works, but newStudents has been doubled to six names and students still holds only the original three.
The fix is to append to students and assign the result back to students:
students := []string{"Andy", "Mary", "Alex"}
newStudents := []string{"Ana", "Alexa", "Mike"}
for _, student := range newStudents {
students = append(students, student)
}
Now newStudents is only read, and students grows from three names to six: [Andy Mary Alex Ana Alexa Mike].
Why append is assigned back
append does not change a slice in place. It returns a slice, because the old backing array may be too small and a new, larger one may have to be allocated. That is why the line reads students = append(students, student). Calling append(students, student) and throwing the result away would leave students unchanged, and Go would reject it with a compile error since the result is unused.
Key takeaways
- A loop whose condition depends on the length of a slice must not make that slice grow on every pass, or it never ends.
- A
rangeloop walks over the elements that existed when it started. - Append to the slice you want to grow, and assign the result of
appendback to it. - Prefer building a second slice over modifying the one you are iterating over.