A Dismissal of the Go Programming Language

A Dismissal of the Go Programming Language

This was originally intended to be a critique, but then I realized I can't be bothered to try programming in Go.

Back in 1992, Bell Labs released Plan 9[a], an operating system built on network transparency (you don't have to care whether a resource is local or accessed over the network), and the concept that every operating system object was represented as a file.

It failed to gain any traction. If I were to take a guess at why, I would sum it up as nobody having a problem that would be solved by network transparency and having everything as a file. While those two features sound good in an academic way, the reality is that they often make things much harder:

  • Network transparency isn't always good: While having a common interface for local and remote resources is possible and good[1], remote resources are fundamentally different from local in terms of latency and reliability. Hiding the difference really only makes it impossible to take those difference into consideration and makes it impossible to reason about system performance and reliability.

  • The file abstraction doesn't fit: A file is (at best) a blob of bytes, with two operations - read and write. Not many things fit comfortably into that mold. It turns out that people really did need all those complex data structures with their complex ways of accessing them.

Enter Go[b], a programming language from Robert Griesemer, Rob Pike, Ken Thompson and the giant company called Google.

Pike and Thompson are known for Plan 9, which is why I brought it up. Just as Plan 9, Go ticks most academic checkboxes: Dynamic typing, lightweight processes (goroutines) and in general a wish to make a systems programming language simpler than C++ in particular. Unfortunately there are many echoes of Plan 9 in this. Just as Plan 9 tried to simplify access to resources but actually made it harder by ignoring real-world constraints, Go does the same. The designers try to help by hiding some complexity, but can only hide it in trivial cases. In every other case, the complexity hits you full-on anyway - but now you have no way of dealing with it because the language designers staked everything on you not having to.

  • No exceptions: Originally, Go only supported the return-value style of error checking. The dictum is that a good programmer will check every return code. We all know how well that policy worked - we're coming up on half a century of C programming and the world has yet to see a single one of these "good" programmers, but we've seen security holes more numerous than the stars. Exceptions, even if they are not handled, keeps the program from thrashing by aborting execution abruptly as soon as it is obvious that the assumptions made by the programmer are invalid. Eventually, Go added the panic / recover mechanism, similar to the setjmp / longjmp facility in C, which gives you all the abrupt termination effects of exceptions but with absolutely no clue as to what is going on.

  • No provision for controlling resource use of goroutines: A goroutine is supposedly a lightweight process. First, goroutines are cooperatively scheduled[2], which brings us back to Windows 3.1 style multitasking, circa 1995, with all the problems of one process bringing the whole system to a halt. Second, how lightweight? Let's say I want to process an image with 24 million pixels. Should I launch one goroutine per pixel? Obviously not, but then how are these things controlled? My own experience is that concurrency must be handled system wide by exposing the available processing resources. Otherwise you leave the programmer to take a wild guess as to how much resources to claim. By hiding necessary complexity, the job is made harder.

  • No generics: Again, "you don't need them", because the language and runtime should be kept simple. Instead, you are expected to use the interface{} "top type" (much like a Java Object or C++ void*). Like using return values for error handling, we know how well that worked out.

  • Shared-nothing: Go tries to solve the problems with shared memory by instead implementing Hoare's CSP, first described in 1978. The reason CSP hasn't caught on in 36 years is that there are very few problems that it solves: The usual target for multicore processing is that you have a big data structure (an image, say) and you want to do something with it. The Go philosophy, Don't communicate by sharing memory; share memory by communicating[*][c], doesn't work because there is no way anyone is going to actually send that image data all over the place. This is not unique for image processing - in most parallel systems, the code goes to the place where the data is, not the other way around, because the code is smaller. Perhaps realizing this, Go does have support for shared memory, locks and all, which means we get all the problems of shared memory plus all the problems of 1995-style cooperative multitasking.

To summarize, Go is the new Plan 9. It's a bag of programming language features, but with no clear way to assemble these into a real advantage[3]; while at the same time coming with lots of disadvantages. Programming languages are not scored on how well they have managed to implement mathematical purity or that elusive attribute, "simplicity". If they did, we'd all be writing self-modifying LISP code.