Go

Go is a freely available systems programming language: http://golang.org/. Go grabbed my attention because Rob Pike is one of its designers. As I now concur with many of his opinions, I was eager to review a language that consumed much of his time.

The rightful heir

Though not backwards compatible with C like C++ once was, I see Go as the true spiritual successor to C. Almost all the gripes in the preceding chapters have been answered. Go builds on the strengths of C and addresses its weaknesses, without succumbing to questionable object-oriented fashions such as inheritance.

That is not to say Go is the next evolutionary step, and simply a patch to C to aid usability. On the contrary, Go is packed with revolutionary changes. Go more than modernizes C; Go pushes into the future. In this new world, there are 3 major shocks for C programmers: interfaces, channels and safety.

Also, anonymous functions and true closures give Go a much stronger functional flavour than C, but I already habitually use GNU C’s nested functions.

Despite the extra features, Go only has 25 keywords, 7 fewer than C89. However, practically speaking, both counts are fictitious. With C, some preprocessor directives deserve to be keywords. With Go, there are many predeclared identifiers, and they outnumber the keywords.

A random sampling

Let’s first examine some of the little features.

No preprocessing required

Like Java, the package and import keywords replace #include directives. The first example from the Go website:

package main

import "fmt"

func main() {
  fmt.Printf("Hello, 世界\n")
}

Incidentally this example also shows off Unicode support. String constants are aware of UTF-8.

The absence of semicolons is striking to a C programmer. It turns out they are only needed when you want multiple statements on the same line, and for separating initialization and conditional expressions in certain constructs.

Type declaration

Declaring variables in C has such an involved syntax that it is an interesting exercise to write a "cdecl" program (a program that translates C variable declarations to English and back).

In contrast, the type of a Go declaration always appears after the variable name and can be read off left to right. For example:

var buf [80]byte

declares buf to be a size-80 array of bytes. A "godecl" program would be much less interesting to write.

Infer, declare, and assign

In C, we can declare and assign variables in one step:

struct foo_s *foo = new_foo();

Go’s ":=" operator boosts brevity further. As with some functional languages, the type of the variable is omitted, and instead inferred from the right-hand expression. The above example would be:

foo := new_foo()  // Same as:  var foo *foo_s = new_foo()

Maps

Built-in support for hash tables makes rapid prototyping easier. Naturally, C programmers often roll their own hash tables. I myself have a little library that I carry around. But the syntax is unavoidably clunkier.

Go acknowledges a desire for a decent built-in hash map with succint notation.

Initiation rites

C’s for loop is a masterpiece of design. It condenses loops such as:

a;
while(b) {
  foo();
  c;
}

into:

for(a; b; c) foo();

The elements of the loop are convenient compacted between the parentheses. No space is wasted on a trivial initialization statement, and another for iteration. Its style almost looks functional.

C++ and C99 improved the for loop by supporting variable declarations in the initialization expression.

Go goes further: if and switch statements have optional initialization expressions:

if v, found := tab["key"]; found {
  fmt.Printf("The hash table contains 'key'->%v.\n", v)
}

On the other hand, Go has no equivalent to the comma operator, which C can use to obtain similar statements.

Switch

Go’s switch statement is almost like syntactic sugar for a series of if-else-if statements, because unlike C, we automatically jump out of the switch block at the beginning of the next case statement. But they are not interchangeable with if-else-if blocks for two reasons:

  1. The fallthrough statement means control flow passes to the next case statement, just as it would with a C case statement not terminated with a break statement.

  2. For some reason, break still has the same meaning as it does in C. It jumps to the end of the switch block. Within an if block, it would jump to the end of the surrounding for loop, not to the end of the if block.

Deferred statements

Suppose a function has to return early, but it should clean up a data structure first:

void f() {
  init();
  if (error()) {
    cleanup();
    return;
  }
  proceed();
  cleanup();
}

However, Duplication Is Evil. If other return statements are added, then they too will require cleanup, unless they occur before initialization perhaps. Either way, one must tread carefully. If more cleanup statements are needed, then we must add them before every return statement.

We could set a flag if an error is detected, and only execute certain statements if the flag is clear. I prefer the briefer:

void f() {
  init();
  if (error()) goto done;
  proceed();
done:
  cleanup();
}

though anti-goto programmers would object.

Go’s defer statement captures the intent, pleasing all sides. It works as a generalized atexit() call:

func f() {
  init()
  defer cleanup()
  if error() { return }
  proceed()
}

Exceptions

However, defer is more than just a pretty keyword. Together with panic and recover, it gives Go clear, clean, and powerful exception handling.

func ff() {
  defer func() {
    if x := recover(); x != nil {
      println("recovering from panic ", x)
    }
  }
  f()
  g()
}
func f() {
  init()
  defer cleanup()
  if error() {
    panic(42)
  }
  proceed()
}

Suppose we call ff(), which calls f(). As before, if error() returns true, panic causes the function to return early, but only after executing the deferred cleanup function.

Control is returned to ff(), but we are still in panic mode. This means this function too will return early; g() never executes. If that were the whole story, the panic would continue to propagate up the call stack, ultimately terminating the program.

However, before ff() returns, a deferred function calls recover(). Normally this function returns nil, but during a panic, it returns the argument given to the triggering panic call (in this case, 42) and restores calm. Although ff() returns early, we are no longer panicking.

If we wanted to continue the panic, we could simply call panic() again.

To be continued

I hope to describe a few more minor features eventually (e.g. raw strings, multiple return values, defer modifying return values, slices, string immutability, iota, arbitrary-precision constants, export conventions).

Interfaces

Go’s interfaces provide a novel delegation mechanism I have never seen before. Interfaces obviate the need for much boilerplate code, whether you’re coming from a C-like world or a Java-like world.

With C, we save on typecasts and function pointer assignments. With Java, we no longer need to use the "implement" keyword so frequently, and declaring a tiny Go interface is much easier than writing a small pure virtual Java class.

With interfaces, we enjoy the convenience and freedom one expects from scripting languages with dynamic typing, even though Go is strongly typed.

In short, rather than declare that type A implements interface B then require the presence of particular methods, Go does the reverse: if particular methods are defined for type A, then it automatically gains the rights and responsibilities of interface B. Though akin to duck typing in Javascript and its ilk, type errors are detected at compile-time rather than run-time.

Channels

Concurrent programming is often difficult. The mere mention of the subject fills me with dread, as it conjures up hours spent investigating bizarre nondeterministic crashes, and puzzling over mutexes, semaphores, condition variables, shared memory, freeing memory in the right thread, thread starvation, the number of cores versus the number of threads, and so on.

Yet I routinely write flawless concurrent programs without a second thought. The only catch is I have to be in a shell, such as bash. For example:

$ ls | less

Pipes are almost magical in this respect. Threads rarely cross my mind when piping commands into one another. Pipes simply get stuff done.

Go’s channels can be described as type-safe pipes. Channels can combine functions in the same manner a shell combines command-line tools. However, Go’s channels are more powerful by an order of magnitude since channels are first-class objects in the language.

Though too expensive for some low-level tasks such as reference counting, their performance is adequate for many applications. Behind the scenes, Go allocates a bunch of worker threads, distributes the work to them, and keeps things thread-safe without programmer intervention.

Channels are in fact relatively old. I first saw them in Limbo, over a decade before Go was released, and had thought most languages would soon adopt a similar construction. I was wrong, for which I blame non-engineering factors: unlike C and Go, Limbo was not freely available soon after its release. On the other hand, competing compiled languages of the time did not have channels, but were freely available.

Safety

Although Go has pointers, pointer arithmetic is forbidden. This implies Go can check array bounds, and handle memory allocation on behalf of the programmer. Go automatically collects garbage, and a pointer never points to somewhere it shouldn’t. More precisely, even if a pointer is pointing at the wrong thing, at least it’s still pointing to memory belonging to our program.

I am somewhat torn. Part of me relishes the old ways, paying attention to every last detail to avoid memory leaks and dangling pointers, in order to squeeze every last drop of performance out of the code. Also, pointer arithmetic can be clearer than array indexing.

On the other hand, C sometimes seems like overkill, so I write shell scripts for the simplest tasks. However, as the task becomes more complex, I graduate straight to C, bypassing scripting languages such as Perl and Python. I reason that if I must type more than a few lines, I may as well use a statically typed compiled language and have it run ten times as fast.

Go might be a good middle ground: it takes care of many annoying details, yet its syntax is familiar and comforting; it won’t frighten away programmers of my generation.

A second opinion

I enjoyed Boyko Bantchev’s review of Go; he also seems to be a fan of C.


Ben Lynn blynn@cs.stanford.edu