One loop to rule them all

Station 1.9 13 min Interchange: for & while & foreach & break 2 → for

PHP has for, while, do … while and foreach, and that's before array_walk, array_map and the Collection pipeline you actually use. Go has for. Just for, in a few shapes. Once you've seen them, there's nothing else to learn about looping.

The shapes of for

The classic three-part loop is PHP's, minus the brackets and dollar signs:

for i := 1; i <= 3; i++ {
	fmt.Printf("Carriage %d\n", i)
}

i is scoped to the loop, like the short statement in an if (1.8). One small difference: i++ is a statement in Go, not an expression, so x = i++ and $a[$i++]-style tricks don't compile.

Condition only is a while loop:

for seats > 0 {
	seats--
}

No condition at all loops until something calls break or return. Go has no do-while, so this is how you write one: loop forever, and check at the bottom.

for {
	line = readLine()
	if line == "" {
		break
	}
}

break and continue work exactly as they do in PHP.

range

range is foreach. It walks a slice (Go's list, Unit 2), a string, a map, or even a number, and hands you two things each time round:

for i, stop := range stops {
	fmt.Printf("%d: %s\n", i, stop)
}

For a slice, that's the index and the value, always in that order. Skip the index with _, as in for _, stop := range stops, and skip the value by leaving it off: for i := range stops.

Mind the gap

One variable means the index, not the value. PHP's foreach ($stops as $stop) gives you the values. Go's for stop := range stops gives you 0, 1, 2, because range always gives the index first. It compiles, it runs, and stop is an int. When you want the values, write for _, stop := range stops.

The value is also a copy. Assigning to stop inside the loop changes nothing in stops, just like PHP's foreach without &. To change the slice, assign through the index: stops[i] = strings.ToUpper(stops[i]). range has no by-reference form like &$stop, which also means no PHP-style "forgot to unset() the reference" bug.

Ranging over a number counts from zero: for i := range 3 gives 0, 1 and 2. It's the neatest way to say "do this n times", as long as you remember it starts at 0 and stops before n.

Ranging over a string gives you runes, as promised in 1.5. The index is the byte position where each rune starts, so range "Köln" gives positions 0, 1, 3 and 4. The ö takes two bytes.

Ranging over a map works too, but in random order on purpose. That's lesson 2.5, and it surprises everyone who's used PHP's ordered arrays.

Laravel corner

Most Laravel code barely writes foreach. It's collect($stops)->filter(…)->map(…)->values(). Go has no Collection pipeline: a plain for … range is the idiomatic answer, and the slices and maps packages (2.6) cover sorting, searching and friends. It feels like a step backwards for about a week. Then you notice you can read any loop without remembering what ->pluck() returns.

Breaking out of nested loops

PHP's break 2 counts levels. Go gives the outer loop a label and breaks out of it by name:

search:
	for _, line := range lines {
		for _, station := range line.Stations {
			if station == target {
				break search
			}
		}
	}

continue search works the same way. It's clearer than counting, and adding a third loop level later doesn't silently change what break 2 means.

Recap

  • for is the only loop: three-part, condition only (while), or bare (forever, until break). There's no do-while.
  • range is foreach. It gives index then value, _ skips the index, and one variable means the index only.
  • The range value is a copy. Change the slice with xs[i] = ….
  • range n counts 0 to n-1. range over a string gives runes, with byte positions.
  • Label the outer loop and break label instead of break 2.

1The classic for

PHPclassic.php
<?php
for ($i = 1; $i <= 3; $i++) {
    echo "Carriage {$i}\n";
}
GOclassic.go
for i := 1; i <= 3; i++ {
	fmt.Printf("Carriage %d\n", i)
}

for i := range 3 { // 0, 1, 2
	fmt.Println(i)
}

Mind the gap

No brackets, and i++ is a statement, not an expression, so x = i++ won't compile. range 3 counts from 0 up to, but not including, 3.

2while, do-while, forever

PHPwhile.php
<?php
while ($seats > 0) {
    $seats--;
}

while (true) {
    // until break
}

do {
    $line = readLine();
} while ($line !== '');
GOwhile.go
for seats > 0 {
	seats--
}

for {
	// until break
}

for {
	line = readLine()
	if line == "" {
		break
	}
}

Mind the gap

There's no while keyword. A for with only a condition is a while loop, and a bare for loops until break. There's no do-while: loop forever and break at the bottom.

3foreach is range

PHPforeach.php
<?php
foreach ($stops as $i => $stop) {
    echo "{$i}: {$stop}\n";
}

foreach ($stops as $stop) {
    echo $stop;
}
GOforeach.go
for i, stop := range stops {
	fmt.Printf("%d: %s\n", i, stop)
}

for _, stop := range stops {
	fmt.Print(stop)
}

Mind the gap

range gives the index first, then the value, always in that order. Use _ to skip the index.

4Breaking out of nested loops

PHPlabels.php
<?php
foreach ($lines as $line) {
    foreach ($line->stations as $station) {
        if ($station === $target) {
            break 2;
        }
    }
}
GOlabels.go
search:
	for _, line := range lines {
		for _, station := range line.Stations {
			if station == target {
				break search
			}
		}
	}

Mind the gap

PHP counts levels with break 2. Go names the loop with a label and breaks out of it by name, so adding a third level never breaks your code.