Start learning free

LESSON 148 OF 338 · LOOPING OVER ARRAY INDICES

Changing Slice Elements in a Go for Loop

Lesson Read the full explanation

Suppose you have an array nums and your task is to increment every number in that array by one.

The first idea that might come to your mind is to write a for loop like this:

nums := []int{1, 2, 3}

// Increment items:
for _, num := range nums {
  num++
}

// Print the array after:
for _, num := range nums {
  fmt.Println(num)
}

but when you run this code, you'll be surprised to see that the output is:

1
2
3

No element in nums changed its value. How is this possible since we clearly do num++ for every num in nums?


Let's talk about copies:

Take a look at this code:

number := 100

numberCopy := number

numberCopy++

fmt.Println(numberCopy) // Output: 101
fmt.Println(number) // Output: 100

Although we wrote numberCopy := number, this doesn't mean that number and numberCopy are one and the same.

numberCopy is a completely different entitity (stored at a different memory address) than number.

What we did is initialize this new numberCopy entity with the value of number. That's why we say the numberCopy is a copy of number.

And so numberCopy++ will only increment numberCopy and won't have any effect on number.


The loop variable is just a copy:

This is what also happens with a for loop.

Whenever you write a for loop, the loop variable (in our case num) is just a copy of the current item of the array, not the item itself.

This code

nums := []int{1, 2, 3}

for _, num := range nums {
  num++
}

works exactly like this code:

nums := []int{1, 2, 3}

num := nums[0]
num++

num = nums[1]
num++

num = nums[2]
num++

Notice that only the variable num is being manipulated, which has nothing to do with the actual entries of the array nums.

You'll see this better if we also print the value of num on each iteration.

nums := []int{1, 2, 3}

for _, num := range nums {
  num++
  fmt.Println(num)
}

// Print the array after:
for _, num := range nums {
  fmt.Println(num)
}

The output of this code is:

2
3
4
1
2
3

Looping over the indices:

In order to manipulate the entries of some array we have to loop over the items' indices:

nums := []int{1, 2, 3}

for index := 0; index < len(nums); index++ {
  nums[index]++
}

This code works exactly like this one:

nums := []int{1, 2, 3}

index := 0
nums[index]++

index++
nums[index]++

index++
nums[index]++

In this way we directly manipulate the actual entries of the list (nums[0], nums[1], etc.)

Assignment:

Fix our code such that it correctly increments (adds one to) the even numbers of nums and decrements (subtracts one from) the odd ones.

STEP 1 OF 1
    Ctrl + Enter to check

    GO · LOOPING OVER ARRAY INDICES

    Buggy Code Manipulate Array in Go, explained

    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:

    1. Produce the index. Replace the range header with a counting loop: for i := 0; i < len(nums); i++.
    2. 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.
    3. Write through the index. The two updates become nums[i] += 1 and nums[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++ and num += 1 count as uses for the compiler, so the broken loop builds cleanly. Check the result, not just that the code compiles.
    • Reading versus writing. A range loop 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 % 2 is -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 a range is a copy.

    Key takeaways

    • In for _, num := range nums, num is 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.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free