How to loop with an index in Go
Use range with two variables: for i, v := range s. The first is the index, the second is a copy of the element. Want only one of them? Drop the other — for i := range s gives just the index, for _, v := range s just the value.
Go has exactly one loop keyword — for — and it wears four hats. The one you want here is the range form, which hands you an index and a value on every iteration of a slice, array, string, map or channel. The older three-clause form (for i := 0; i < len(s); i++) still earns its keep when you need to walk backwards or step by more than one. This page covers both, the blank identifier that throws half the pair away, the byte-offset surprise you get when the thing you're ranging is a string, and the Go 1.22 change to how the loop variable is scoped. Each example runs on this page — hit Run, then edit the code and run it again.
1for i, v := range sRecommended
Range with two variables is the idiomatic Go loop. The first variable is the index (an int, starting at 0), the second is the element. The names are yours — i, lang reads better than i, v — and the bounds are the compiler's problem, not yours, so an off-by-one is impossible. The one thing to internalise: the second variable is a copy. Assigning to it changes the loop variable, never the slice.
Output
Prints 0 go, 1 rust, 2 zig, then [go rust zig] — the middle loop assigned to a copy and the slice never noticed — and finally [go! rust! zig!], where the write went through languages[i]. That copy also has a cost: ranging a []BigStruct copies the whole struct every iteration, so for large elements prefer for i := range s and touch s[i] directly. Range evaluates its expression once, so appending inside the loop can't make it run forever.
2Index only, value only, or neither
Range is happy with fewer variables. Write for i := range s and you get the index alone — the form to use when you intend to write into the slice. Write for _, v := range s and the blank identifier _ discards the index; Go rejects unused variables, so _ is how you say “I know, I don't want it”. Drop both and for range s simply repeats. Need human-facing numbering? There is no start= option like Python's enumerate — just print i+1.
Output
Prints the numbered list 1. write tests … 3. ship it, then the same three tasks bulleted, then ran 3 times. Go 1.22 added a fourth spelling for “do this n times”: for i := range 3 ranges over an integer and yields 0, 1, 2, with for range 3 as the no-variable version. The hosted Go 1.26 compiler supports both forms. The snippet above ranges over tasks because its count should stay tied to the actual slice length.
3The three-clause loop: backwards and custom steps
for init; condition; post {} is the C-style loop, and it is the right answer whenever the index doesn't march 0, 1, 2 …. Counting down, stepping by two, stopping early on a computed bound, or advancing the index by a variable amount inside the body are all things range simply cannot express. For a plain forward walk, though, range is shorter and safer — reach for this form only when you need what it gives you.
Output
The first loop prints 0 2 through 4 11; the reverse walk collects [11 7 5 3 2] and the stepped one [2 5 11]. Two details worth knowing: the condition is re-evaluated every iteration, so len(primes) is read each time round (fine — it's an O(1) field read, and it means the loop notices if the slice shrinks), and i is scoped to the loop, so it does not exist afterwards. If you need the final value, declare i outside. Going backwards over characters? Convert to []rune first — see reversing a string in Go.
4Ranging a string gives byte offsets, not character indexes
This is the one that bites. A Go string is a slice of bytes, conventionally UTF-8, and for i, r := range s decodes it: r is a whole rune (a Unicode code point), but i is the byte offset where that rune starts. So the indexes jump. Anything outside ASCII takes two to four bytes, and the offsets skip the gap.
Output
The offsets come out 0, 1, 2, 3, 5, 6 — there is no 4, because é occupies bytes 3 and 4, and no 7 or 8 because 語 takes three bytes. The string is 9 bytes, 6 runes. Indexing with s[3] doesn't give you a character at all: uint8 195, the first byte of the two that encode é. If you want an index that counts characters, convert once with []rune(s) and range that — runes[3] is 'é', as you'd expect. That conversion allocates, so do it once outside the loop, never inside it.
5The loop variable, closures, and the Go 1.22 change
Before Go 1.22, i and v were one variable reused for the whole loop. Every closure created inside the body captured that same variable, so by the time you called them they all saw the final value — the notorious “every goroutine printed the last item” bug. Go 1.22 changed the spec: in a module declaring go 1.22 or later, the loop variables are fresh on every iteration, and the bug is gone. The explicit fix still works everywhere and still documents intent, which is what the snippet below uses: shadow the variable with i, item := i, item, or pass it into the goroutine as an argument.
Output
Prints 0:a, 1:b, 2:c, then [0=a 1=b 2=c]. Being honest about the runner: the interpreter behind these embeds still follows the pre-1.22 rule, so if you delete the i, item := i, item line and hit Run, all three closures report the same, final values instead of 0:a, 1:b, 2:c — the historical bug, live. A real Go 1.22+ toolchain gives the right answer either way. That difference is exactly why the explicit copy is worth keeping in code that has to run on more than one toolchain. Note also that the goroutine results are written into fixed slots of out rather than printed directly — goroutine scheduling order is never guaranteed, and no language version fixes that.
6Which should you use?
| Form | Gives you | Works on | Best for |
|---|---|---|---|
| for i, v := range s | Index + value | Slices, arrays, strings, maps, channels | Almost everything |
| for i := range s | Index only | Slices, arrays, strings, maps | Writing back into the slice; big elements |
| for _, v := range s | Value only | Slices, arrays, strings, maps, channels | Read-only passes |
| for range s | Neither | Anything rangeable | Repeating len(s) times; draining a channel |
| for i := 0; i < len(s); i++ | Index only | Anything with len() | Backwards, custom steps, skipping ahead |
| for i := range n (Go 1.22+) | Index only | An int | A fixed repeat count, no slice in sight |
Frequently asked questions
How do I get the index in a Go for loop?
Use the two-variable range form: for i, v := range s. The first variable is the index and the second is a copy of the element. Go has no enumerate() — range is the built-in equivalent, and it works on slices, arrays, strings, maps and channels.
What does for i := range s do with only one variable?
It gives you the index alone and skips the element copy entirely. That is the form to use when you want to write into the slice (s[i] = ...), because the value from a two-variable range is a copy and assigning to it does nothing. It is also the cheaper form when the elements are large structs.
How do I loop a fixed number of times in Go?
On Go 1.22 and later, range over an integer: for i := range 10 yields 0 through 9, and for range 10 repeats ten times without binding a variable. On older toolchains use the three-clause form, for i := 0; i < 10; i++.
Why do all my goroutines see the same loop variable in Go?
Before Go 1.22 the loop variable was a single variable reused across iterations, so every closure captured the same one and saw the final value. Go 1.22 made loop variables per-iteration, which fixes it for modules declaring go 1.22 or later. To be correct on any version, make the copy explicit — i := i inside the body, or pass the value in as a goroutine argument.
Why does my index skip numbers when I range over a Go string?
Because the index is a byte offset, not a character position. Ranging a string decodes UTF-8, so the rune is whole but the offset advances by however many bytes that rune took — 1 for ASCII, up to 4 otherwise. For character indexes, convert once with []rune(s) and range the rune slice; utf8.RuneCountInString(s) counts characters, while len(s) counts bytes.
How do I loop backwards or start counting at 1 in Go?
For backwards, use the three-clause loop: for i := len(s) - 1; i >= 0; i--. Range only ever goes forwards. To number output from 1, keep ranging and print i+1 — Go has no equivalent of Python's enumerate(items, start=1).