skip to content

In Go, what does range yield for a slice, a map, a string, and an integer?

level: middleimportance: must knowfreq 74%

answer

  1. the first value is not always an index
  2. one variable gives only the first
  3. a map hands over key and value
  4. a string counts bytes, decodes runes
  5. an integer counts up from zero

basics

~20 s

A slice yields the index and a copy of the element; a map yields a key and its value in unspecified order; a string yields the byte offset and the decoded rune; an integer n yields the values 0 through n-1.

solid answer

~50 s

The `range` clause hands the body up to two values, and what they mean depends entirely on what you range over. Over a slice or array you get the index and a copy of the element. Over a map you get the key and the value, in an order that is deliberately randomised and must not be relied on. Over a string you get the byte offset at which each rune starts and the rune itself as an `int32`, not a byte. Over an integer `n` you get a single value counting 0, 1, ... n-1 - a form added in Go 1.22 - and a non-positive `n` gives zero iterations. Taking one variable gives you only the first of the pair (index, key, byte offset, or counter), and `for range x { }` with no variables at all is legal when you just want the repetitions.

code

go · 13 lines
go
s := []string{"id", "name"}
for i, v := range s { // index, copy of the element
	_, _ = i, v
}

m := map[string]int{"a": 1}
for k, v := range m { // key and value; order is not specified
	_, _ = k, v
}

for i := range 3 { // Go 1.22+: i is 0, then 1, then 2
	_ = i
}

go deeper

for a junior

Memorise the pairs: index and element for a slice, key and value for a map. Remember that a single variable gives you the index, not the element - that is the mistake that costs juniors the most time.

for a middle

Explain all four operands including the byte offset for strings and the 0-to-n-1 counter for integers, and say plainly that map order is randomised on purpose. Expect to be asked what the one-variable form yields in each case.

for a senior

Show that you design around the randomised map order rather than being surprised by it - sort keys when output must be reproducible, and treat a test that depends on map order as a broken test.

for a principal

Own the readability argument: a codebase that ranges consistently, with _ for unwanted positions and sorted keys where output is compared, removes a whole class of flaky tests and review comments. Be able to state that as a convention worth enforcing.

## What range actually gives you `range` is a clause on the `for` statement, and it is the only loop form in Go that produces values for you. The shape of what it produces changes with the operand, which is why saying "range gives you the index and the value" is only a quarter true. ### Over a slice or an array ```go for i, v := range names { // i is an int index, v is a copy of names[i] } ``` The first value is the `int` index, starting at 0. The second is a **copy** of the element, assigned fresh into `v` each iteration. Ranging over an array value rather than a slice behaves the same way from the body's point of view. ### Over a map ```go for k, v := range counts { // k is the key, v is a copy of the value stored under it } ``` You get key and value. The **iteration order is not specified and is deliberately randomised** by the runtime, so two runs over the same map can visit entries in different orders. This is not an accident to be worked around; it exists so that programs cannot accidentally depend on an order the implementation never promised. If you need deterministic output - writing a CSV report, say - collect the keys into a slice, sort them, and range over the sorted slice. ### Over a string ```go for i, r := range line { // i is the byte offset where r begins, r is a rune (int32) } ``` A Go string is a sequence of bytes, conventionally UTF-8. Ranging over it decodes one UTF-8 sequence per iteration: the second value is the code point as a `rune`, and the first is the **byte offset** at which that code point starts - not a character counter. For pure ASCII the two coincide, which is exactly why the difference bites only when real user data shows up. Indexing a string with `line[i]` instead gives a single `byte`, which is a different thing again. ### Over an integer ```go for i := range 5 { // i is 0, 1, 2, 3, 4 } ``` Since Go 1.22 the operand may be an integer. The loop yields a **single** value counting from 0 up to n-1, and taking two variables does not compile. If `n` is zero or negative the body never runs. This form replaces the boilerplate `for i := 0; i < n; i++` when the counter is all you need, and `for range n { }` with no variable is the clean way to repeat something n times. ### The one-variable and zero-variable forms Every range form lets you take fewer values than it offers. With one variable you get only the first: the index for a slice, the key for a map, the byte offset for a string, the counter for an integer. With none at all - `for range x { }` - you get the iterations and nothing else, which is how you count entries or simply repeat. There is a small trap here for people arriving from Python or JavaScript, where `for x in xs` binds the element. In Go, `for x := range xs` over a slice binds the **index**. Writing `for _, v := range xs` when you want values is the habit to build. ### Unused variables are a compile error Go rejects a declared-and-unused local variable, and range variables are no exception: `for i, v := range xs` where `v` is never read does not compile. Use the blank identifier `_` for the position you do not need. That is why so much real Go reads `for _, row := range rows`. ### Practical shape in a batch job A job that reads a directory of CSV files typically uses three of these forms within a few lines: range over a slice of file paths, range over a slice of parsed columns with the index when a column has to be rewritten in place, and range over a map of per-column totals when the summary is emitted - with a sort first if the output has to be stable across runs. Being able to say which of the four operands you are looking at, and therefore what the two variables mean, is the whole skill here. ### What range does not do here The operand is evaluated once, before the first iteration, and the second value is always a copy rather than an alias into the collection. Those two properties are where the real surprises live, and they are worth studying separately from the yield shapes above.

  • Can you take two variables when ranging over an integer?
    No. The integer form yields exactly one value, the counter from 0 to n-1, so `for i, v := range 10` does not compile. Use `for i := range 10` when you want the counter and `for range 10 { }` when you only want ten repetitions. A zero or negative operand produces no iterations at all rather than an error.
  • Why is map iteration order randomised rather than merely unspecified?
    Because an implementation detail that happens to be stable gets depended on. The runtime starts each iteration at a random bucket so that a program relying on order fails quickly and visibly instead of in production after an unrelated change. When you need a stable order, extract the keys into a slice, sort them, then range over that slice.
  • Is for range x { } with no variables at all legal?
    Yes. The variable list is optional, so `for range items { count++ }` and `for range 3 { retry() }` both compile. It is the idiomatic way to say "do this once per element" or "do this n times" when neither the index nor the element is wanted, and it avoids a blank identifier that carries no information.

saying these in an interview costs you the question

  • Says map range returns entries in insertion order
  • Thinks the string index is a character position
  • Assumes one variable gives the element, not the index
  • Claims range over an integer yields 1 through n
  • Believes ranging a string yields single bytes