Kristian Glass - Do I Smell Burning?

Mostly technical things

Switch Statements - a C/Go Gotcha

One of my side-projects at the moment involves me porting an old-ish (20+ years) large-ish (100,000+ lines of code) network game server from C to Go.

Thanks to cgo, similarities between C and Go, and the power of (some horrific) Vim regular expressions and macros, this is less epic than it might initially sound!

However, one thing has repeatedly stood out in the process, and will inevitably have me tearing my hair out in future due to the subtle bugs I’ve introduced as a result…

Here is a short C program with a switch statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

void s(int i) {
  switch (i) {
      case 1:
      case 2:
          printf("Foo\n");
          break;
      case 3:
          printf("Bar\n");
      case 4:
      case 5:
          printf("Baz\n");
  }
}

int main() {
  for (int i = 0; i < 5; i++) {
      printf("i: %d\n", i);
      s(i);
  }
}

And here is a short Go program with a switch statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "fmt"
)

func s(i int) {
    switch i {
    case 1:
    case 2:
        fmt.Printf("Foo\n")
        break
    case 3:
        fmt.Printf("Bar\n")
    case 4:
    case 5:
        fmt.Printf("Baz\n")
    }
}

func main() {
    for i := 0; i < 5; i++ {
        fmt.Printf("i: %d\n", i)
        s(i)
    }
}

If you’re familiar with only one of the two languages, or just casually glancing, you might assume they had the same output. They do not.

Here is the output of the C code:

1
2
3
4
5
6
7
8
9
10
11
$ gcc switch.c && ./a.out
i: 0
i: 1
Foo
i: 2
Foo
i: 3
Bar
Baz
i: 4
Baz

And of the Go:

1
2
3
4
5
6
7
8
$ go build switch.go && ./switch
i: 0
i: 1
i: 2
Foo
i: 3
Bar
i: 4

Why?

In C, switch statements are basically “jump to the first matching case statement, keep executing until you hit a break or the end of the switch block” (which allows some “interesting” techniques like Duff’s Device and this coroutine implementation).

So in the C example, when x is 1, execution “jumps to” the case 1, continues into case 2, prints Foo, and then hits the break and leaves the switch.

In Go, case blocks are more like entities in their own right – “if the conditions of this block match, execute the block”.

So in the Go example, when x is 1, the case 1 block is executed, it’s empty so nothing happens, and that’s it.

A common error in C is to forget to put a break at the end of a case block and then have multiple cases execute. Go avoids this by treating the blocks as self-contained units, which I see as much more intuitive and better. What Go currently (1.11) doesn’t do is warn on unnecessary breaks at the end of a block, as one might have if trying to write Go with the C model in mind.

Both behaviours are reasonable, both are fine once you get them, but when you’re switching back and forth between the two languages, good luck making sure you don’t fall into this trap!

Comments