switch vs match

Station 1.10 11 min Interchange: switch & match & match (true) → switch

PHP has two of these. switch compares loosely and falls through unless you remember break. match, from PHP 8, compares strictly, never falls through, and returns a value. Go's switch sits between them, a lot nearer to match.

No breaks to forget

switch line {
case "Central":
	colour = "red"
case "Jubilee":
	colour = "grey"
default:
	colour = "black"
}

Each case ends where the next one starts. There's no break, because there's no fallthrough to stop.

case 'Central'$colour = 'red'; and no break
case 'Jubilee'$colour = 'grey'; runs as well
Bug report"Why is the Central line grey?"
Go cases stop at the next case. There's no break to forget.

A few more things that work like match:

  • Several values share a case with commas: case 301, 302, 307, 308:.
  • Comparisons are strict. Both sides must be the same type, so "1" never matches 1, and the compiler stops you trying.
  • Cases are tried top to bottom, and the first match wins. default can go anywhere, but it only runs if nothing else matches.
  • Cases can be any expression of the right type, not only literals: case maxDelay: or case limit + 1:.

If you really do want PHP's fallthrough, Go has a keyword for it: end a case with fallthrough, and the next case's body runs too, without checking its value. You'll almost never see it.

The difference shows up the moment a case matches:

%%{init: {"flowchart": {"nodeSpacing": 18}}}%%
flowchart TB
  accTitle: What happens after a case matches, in PHP and in Go
  accDescr: In PHP, when case 301 matches, its empty body runs and then execution falls into case 302's body, and on through every case below until it reaches a break. In Go, when case 301 matches, only its own body runs, which is empty, and then the switch ends.
  subgraph go ["Go"]
    direction TB
    g1["301 matches"] --> g2["empty body runs"] --> g3["switch ends"]
  end
  subgraph php ["PHP"]
    direction TB
    p1["301 matches"] --> p2["empty body runs"] --> p3["falls into 302"] --> p4["…to a break"]
  end

Mind the gap

An empty case does nothing. In PHP, stacking case 301: case 302: with no body lets 301 fall through to 302's code. In Go, case 301: with no body matches 301, runs nothing and leaves the switch. It compiles without a word. List the values together instead: case 301, 302:.

Where it's not match

Go's switch is a statement, not an expression. There's no $label = match (…) { … }, so you assign inside each case, or put the switch in a small function and return from each case. That second option reads almost exactly like a match.

And when nothing matches and there's no default, nothing happens. PHP's match would throw an UnhandledMatchError. Go just carries on after the switch, so if a missing case would be a bug, add a default that says so.

The tagless switch

Leave out the value after switch, and each case becomes a condition of its own:

switch {
case delay <= 0:
	band = "On time"
case delay < 15:
	band = "Late"
default:
	band = "Very late"
}

The first case that's true runs. This is PHP's match (true) trick, built in, and it's the usual Go replacement for a long if / else if chain. You can't mix the two styles: a switch code compares every case with code, so a condition like case code >= 500: in one is a compile error.

Like if (1.8), a switch can run a short statement first, switch s := status(); s { … }, with s scoped to the switch.

break in a switch

break works inside a switch, though you rarely need it. The catch is that it leaves the innermost for or switch. Inside a loop, a break in a case leaves the switch, and the loop carries on. To leave the loop, label it and use break label, as in 1.9.

(There's one more kind, the type switch, which picks a case based on a value's type. It needs interfaces, so it waits for lesson 3.6.)

Recap

  • No fallthrough and no break needed. An empty case does nothing.
  • case a, b, c: shares a case. Comparisons are strict, and cases are tried in order.
  • A switch is a statement, not an expression. Assign or return in each case. No match means no error.
  • switch { case cond: … } is the tagless switch: Go's match (true) and if / else if replacement.
  • break leaves the switch, not the surrounding loop.

1switch, without the breaks

PHPswitch.php
<?php
switch ($line) {
    case 'Central':
        $colour = 'red';
        break;
    case 'Jubilee':
        $colour = 'grey';
        break;
    default:
        $colour = 'black';
}
GOswitch.go
switch line {
case "Central":
	colour = "red"
case "Jubilee":
	colour = "grey"
default:
	colour = "black"
}

Mind the gap

Go cases don't fall through, so there's no break to forget. Each case ends at the next case.

2match, as a switch

PHPmatch.php
<?php
$label = match ($status) {
    Status::OnTime => 'On time',
    Status::Delayed, Status::Late => 'Delayed',
    Status::Cancelled => 'Cancelled',
};
// no arm matches: UnhandledMatchError
GOmatch.go
func label(s Status) string {
	switch s {
	case OnTime:
		return "On time"
	case Delayed, Late:
		return "Delayed"
	case Cancelled:
		return "Cancelled"
	}
	return "Unknown" // no case matches: carry on
}

Mind the gap

Like match, Go compares strictly and lists several values with commas. Unlike match, a switch isn't an expression, so you assign or return inside each case. And when nothing matches, nothing happens: no error.

3match (true) is a tagless switch

PHPranges.php
<?php
$band = match (true) {
    $delay <= 0 => 'On time',
    $delay < 15 => 'Late',
    default => 'Very late',
};
GOranges.go
var band string
switch {
case delay <= 0:
	band = "On time"
case delay < 15:
	band = "Late"
default:
	band = "Very late"
}

Mind the gap

Leave out the value after switch and each case becomes its own condition. The first true one wins. It's Go's tidy replacement for long if / else if chains.