The problem: a loop that changes nothing
Suppose you want to add one to every number in a slice. The first idea is a range loop:
nums := []int{1, 2, 3}
for _, num := range nums {
num++
}
fmt.Println(nums)
The output is [1 2 3]. The code compiles, runs without errors, and leaves the slice exactly as it was. Go does not complain, because num++ is a perfectly valid statement; it just changes something other than what you meant.
Copies in Go
Assigning one integer variable to another copies the value. After that, the two variables are independent:
number := 100
numberCopy := number
numberCopy++
fmt.Println(numberCopy) // 101
fmt.Println(number) // 100
The variable numberCopy is a separate piece of memory that started with the value of number. Incrementing it has no effect on the original.
The loop variable is a copy
The same thing happens in a range loop. On each iteration, Go copies the current element into the loop variable num. The loop above behaves like this:
num := nums[0]
num++
num = nums[1]
num++
num = nums[2]
num++
Only the variable num is incremented. The slice is never written to. Printing num inside the loop shows the increments happening to the copy:
for _, num := range nums {
num++
fmt.Println(num)
}
fmt.Println(nums)
2
3
4
[1 2 3]
The fix: loop over the indices
To change an element, write to the element itself, which you reach through its position: nums[i]. So the loop has to produce the index.
nums := []int{1, 2, 3}
for index := 0; index < len(nums); index++ {
nums[index]++
}
This works like writing nums[0]++, then nums[1]++, then nums[2]++. Each statement modifies a slice element directly, and the result is [2 3 4].
A range loop can do the same job if you keep the index and ignore the copy: for i := range nums { nums[i]++ }. The index-only form gives i and nothing else, so the only way to touch the data is through nums[i].
The exercise
The task is to make nums, which starts as 1, 2, 3, 4, 5, end up as 0, 3, 2, 5, 4: every even number goes up by one and every odd number goes down by one. The starting code has the right idea but changes the copy:
nums := []int{1, 2, 3, 4, 5}
for _, num := range nums {
if num % 2 == 0 {
num += 1
} else {
num -= 1
}
}
It compiles and leaves nums as [1 2 3 4 5]. Fixing it takes three changes, all in the loop:
- Produce the index. Replace the
rangeheader with a counting loop:for i := 0; i < len(nums); i++. - Read through the index. The test becomes
nums[i] % 2 == 0. This reads the real element, but the test would work with a copy as well. - Write through the index. The two updates become
nums[i] += 1andnums[i] -= 1. This is the change that matters, because it modifies the slice.
nums := []int{1, 2, 3, 4, 5}
for i := 0; i < len(nums); i++ {
if nums[i] % 2 == 0 {
nums[i] += 1
} else {
nums[i] -= 1
}
}
Trace it: 1 is odd and becomes 0, 2 is even and becomes 3, 3 becomes 2, 4 becomes 5 and 5 becomes 4. Printing nums gives [0 3 2 5 4].
Gotchas
- No warning for a useless copy. Go rejects variables that are declared and never used, but
num++andnum += 1count as uses for the compiler, so the broken loop builds cleanly. Check the result, not just that the code compiles. - Reading versus writing. A
rangeloop is fine for reading values, for example to sum them. It is the writes that need the index. - The sign of
%. The remainder takes the sign of the left operand in Go, so-3 % 2is -1. Testing for evenness with== 0, as here, is safe for negative numbers too. - The slice itself is shared. When you pass a slice to a function, the function receives a view onto the same elements, so writing
nums[i] = ...inside the function changes the caller's data. Only the loop variable of arangeis a copy.
Key takeaways
- In
for _, num := range nums,numis a copy of the current element. Changing it never changes the slice. - To modify elements, loop over the indices and write to
nums[i]. - A loop that changes nothing still compiles and runs, so verify the result.