Home

Go's range clause

Saturday 22 June 2013

Go's range clause is powerful, but requires knowledge of the rest of Go to be used properly.

Go's language specification is remarkably easy to read, and the part concerning range clauses is reasonably short. However, unless you're familiar with other aspects of the language, range may surprise you. Since they touch so many other areas of Go, range clauses make a good test of your understanding. All examples have links to the playground where you can run them.

To summarise (and omit some details):

  1. The expression on the RHS is evaluated once, at the beginning.
  2. The variables on the LHS are assigned once per iteration.

The second rule (2) tells us that if we modify the iteration variables (most commonly denoted by i and v, for index and value), then any changes will be ignored and overwritten when the next iteration starts. We can't skip over values using a range clause:

	slice := []string{"north", "east", "south", "west"}
	for i, v := range slice {
		fmt.Println(i, slice[i], v)
		//we can't skip to the end by modifying i
		i = 3
		//we can't modify the slice by changing v
		v = "down"
	}

Slices

Understanding slices is essential to writing idiomatic Go. Slices act like references to the underlying array, so when the expression on the RHS of the range clause is evaluated at the start (1), its length is fixed for the whole loop. If we reslice within the loop, the RHS is not re-evaluated (1). i and v are assigned their values as normal (2), but trying to access slice[i] will cause a panic.

	slice := []string{"north", "east", "south", "west"}
	for i, v := range slice {
		fmt.Printf("%v %v ", i, v)
		//will panic once i = 3
		fmt.Printf("%v\n", slice[i])
		slice = slice[:3]
	}
But since slices point to an underlying array, we can modify the slice as we iterate over it. Note that v is given the updated value of slice[i] at the beginning of each iteration (2).
	for i, v := range slice {
		fmt.Println(i, slice[i], v)
		//change the next value of v
		if i != len(slice)-1 { //avoid out of bounds access
			slice[i+1] = strings.ToUpper(slice[i+1])
		}
	}

Arrays

Arrays (unlike slices) are values (as opposed to pointers). That means no two arrays use the same memory, and assignment involves a copy. When we evaluate the array at the beginning of the loop (1), we create a copy - so modifications to "the array" are not reflected in the iteration variables, so v has the value of a[i] in the original array.
	var a = [4]string{"north", "east", "south", "west"}
	for i, v := range a {
		//modifying an array will cause a[i] to change
		//but v will refer to the value of a[i] when the
		//loop started
		fmt.Println(i, a[i], v)
		if i != len(a)-1 {
			a[i+1] = strings.ToUpper(a[i+1])
		}
	}

If you don't want to create a copy of an array when you loop over it, you can loop over a pointer to an array. I'm not sure what advantage there is over slicing the array - they appear to be equivalent.

	var a = [4]string{"north", "east", "south", "west"}
	//loop over pointer to array
	for i, v := range &a {
		fmt.Println(i, a[i], v)
		if i != len(a)-1 {
			a[i+1] = strings.ToUpper(a[i+1])
		}
	}

	var b = [4]string{"north", "east", "south", "west"}
	//loop over slice
	for i, v := range b[:] {
		fmt.Println(i, b[i], v)
		if i != len(b)-1 {
			b[i+1] = strings.ToUpper(b[i+1])
		}
	}

Strings

Strings in Go are treated as UTF-8 encoded Unicode. Ranging over them exposes you to the nature of Unicode:

	greeting := "Hello, 世界"
	for i, v := range greeting {
		fmt.Printf("%2d %c %c\n", i, greeting[i], v)
	}

Note that in some cases, v != greeting[i]. In fact, they are of different types, so that test wouldn't even compile! v contains successive Unicode code points, and has type rune. i is the byte position of that code point in the string, and greeting[i] is the byte at that point. So i does not increase by one each time, as Unicode code points can be more than one byte long. greeting[i] may just take the first byte of each Unicode code point, and when displayed as a character (using "%c"), shows a different character to one you might expect.

Strings in Go are immutable, so there are no factors to consider about modification when ranging.

Maps

Maps are widely used in Go, and have been given a prominent position in the language. Personally, I find this makes me a lot more inclined to use them than in other languages, resulting in simpler code.

Ranging over maps has some special rules. But to simplify, my guidelines are:

If you don't follow the last two, the result is still safe - it won't corrupt memory or seg fault or leak - but you may have subtle logic errors. But it might be ok. Hence "guidelines".

Channel

Ranging over a channel receives successive values from it until it closes, causing the loop to end.

Be careful about sending to the same channel you're receiving on. If that is the only goroutine that's receiving on that channel you will deadlock:

	c := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			//send on c
			c <- i
		}
		close(c)
	}()
	for i := range c {
		fmt.Println(i)
		//This send will block forever, since the only way
		//it can continue is in the next iteration of this
		//loop, which will only come once we've finished sending.
		c <- i * 2
	}

Don't be tempted to fix this with a buffered channel! Code like that should smell bad.

An example of Go's consistency is ranging over a nil channel. Just like receiving from a nil channel or using one in a select statement, it blocks forever.

Quiz

Does this loop forever?

	slice := []rune{'a', 'b', 'c', 'd'}
	for i := range slice {
		fmt.Printf("%c\n", slice[i])
		slice = append(slice, slice[i]+1)
	}
Answer.

Conclusion

Go's range clause is both useful and widely used. Go is a small, simple language - and I think that is its greatest feature. Using range properly requires a good knowledge of the aspect of the language that you're using it in - but it is consistent and flexible.


Previous (Well behaved goroutines)
Next (A Lazy Sieve of Eratosthenes)