On Go (original) (raw)

Updated 2015-09-25: So, about six years later, people are still reading this. I'm not sure how to feel about this; Looking at it now, it's incoherent, badly argued, and a lot of the details are simply wrong. But 1000 hits a month indicate that people still get value out of it.

Don't get me wrong; I still think that Go is a terrible language. I now work for a company that does a lot of it, and every time I touch it it depresses me a little more (and I've had the 'Hey, did you read that article which compares Go to...' 'Yeah. I wrote it.' 'Oh. awkward silence' conversation at least twice). However, I can now express my reasons in a much more coherent fashion.

I'm going to leave this up here, unmodified, as a historical artifact; but I still wish it were better written.

(If you've enjoyed this article, go read my writeup of Ada! It's a surprising modern and nice language.)

Updated 2009-11-16: Well, it turns out people read this article. Today so far I have had 5841 hits on my website. I normally get about 1500 a month. Thank you all! I'm glad people seem to find it interesting, as I mainly wrote it to prompt discussion.

However, it's come to my attention that most of the Go examples were wrong. Sorry. That was very sloppy on my part. I have corrected these. In addition, I have added sections on switches and union types, as they're interestingly different. Other than that, I haven't changed the bulk of the text (except where other changes required it, and correcting a few typos), despite_much_ temptation.

Yes, I know I use too many ellipses. You know who you are...

Updated 2009-11-17: It appears I got the examples in the Union section the wrong way round. D'oh. Fixed.

Introduction

On November 10 2009, Googleannounced the release of a new programming language that a internal group had been working on:Go. (Although its name might change toIssue 9 real soon now.) Go is a garbage-collectedAlgol-descended language which compiles into real machine code. It's got strong typing, bound checking, built-in support for concurrency and is suitable for writing low-level code as well as high-level code.

In this article I'm going to do a quick tour of the language, comparing it with another established language, which I'm not going to name right now but will instead call Brand X. Brand X is_also_ a garbage-collected Algol-descended language, compiling into real machine code, with strong typing, bounds checking, built-in support for concurrency, etc. It's a real programming language, although I suspect that most readers will not have encountered it before.

How well will Go stand up against Brand X? Let's see.

Program structure

Let's have a minimal example first, just to see how they work.

Go Brand X
package main import fmt "fmt" func main() {   fmt.Printf("Hello, world!\n"); } proc main = void: (   print(("Hello, world!", newline)) ); main

You can see that in Go, we're defining a traditional C-style main function which is calling out to an external printf routine. Brand X doesn't need an explicit main function --- you can put statements in the body of the program. However, to avoid turning this into a boring one-liner, we're demonstrating the definition of a parameterless function returning void that prints our message, which we're then calling from the top level (note that you don't need an empty parameter list on a function call). The double parentheses is due to a quirk of Brand X's print() function: it only accepts a single parameter, so if we want to print more than one item, we have to pass it an array literal.

You may notice that Go is using braces but Brand X is using parentheses. In fact, Brand X doesn't distinguish between blocks and expressions. ; is actually an operator that glues expressions together, returning the value of the last expression (which is why the print() call doesn't have one after it). If you don't like parentheses, you can use begin and end --- they're equivalent.

Let's have a slightly meatier example.

Go Brand X
func positivequadratic(a float, b float, c float) float {   return (-b + sqrt(b*b - 4*a*c)) / (2*a); } proc positivequadratic = (real a, real b, real c) real:   (-b + sqrt(b*b - 4*a*c)) / (2*a);

No surprises here --- the languages are more alike than different. Go puts its types after the parameters; Brand X before. Personally, I like them before, but that's largely a personal aesthetic choice. (Although Go's pointer syntax can make pointer declarations rather unclear.)

Go can return multiple values from functions using a simple Lua-like syntax, and assign them to multiple variables in a single operation. Brand X can't, although it can use reference parameters to achieve a similar effect; more on references later.

Go Brand X
func triad(i int) (int, int, int) {   return i, i+1, i+2; } /* or equivalently */ func triad(i int) (out1 int, out2 int, out3 int) {   out1 = i;   out2 = i+1;   out3 = i+2;   return; } proc triad = (int i, ref int out1, ref int out2, ref int out3): (   out1 := i;   out2 := i+1;   out3 := i+2 )

Go's syntax for declaring the type of the return parameters is simply to append another parameter block to the end of the function declaration. I do have to say that I'm not keen on this --- the lack of visual clues makes it hard to read. On the other hand, it is consistent with how the language works; if Brand X supported multiple return types, it would probably do the same thing.

Control structures

Both languages have the conventional set of control structures --- although Brand X does have a few surprises.

Here's an if:

Go Brand X
if (x > 0) {   print("x is positive"); } else {   print("x is not positive"); } if (x > 0) then   print("x is positive") else   print("x is not positive") fi

You can see the different philosophy of Go and Brand X at work here with regard to the blocking. Go uses C-like statement blocking, where statements are grouped in braces --- note that unlike C, Go requires braces around single statements. Brand X, however, delimits expressions with keywords.

Here are some loops:

Go Brand X
/* counting loop */ sum = 0; for i := 0; i <= 10; i++ {   sum = sum + i; } /* equivalent to C while */ _for_ (sum > 1) {   sum = sum / 2; } /* infinite loop */ for {   playwiththefairies(); } # counting loop # sum := 0; for i from 0 to 10 do   sum := sum + 1 od; # equivalent to C while # while (sum > 1) do   sum := sum / 2 od; # infinite loop # do   playwiththefairies od

At first there's nothing spectacularly different here. Go unifies all of its repetition constructs onto a single keyword,for, which makes the while loop a bit startling to C and Java trained eyes. Brand X, on the other hand, uses more traditional syntax... or does it?

In fact, Brand X does the same thing as Go does. All repetition loops are variants of the do...od construct. The full form is:

for i from 0 to 10 by 1 while condition do ~ od

...and all the modifier keywords are optional. Remove them all and you get an infinite loop, as shown above. Some odd combinations are possible, and useful...

keep incrementing i from 0 indefinitely, until the flag is set

for i from 0 by 1 while not cancelled do ~ od

run loop 10 times, but don't bother with a counter

to 10 do ~ od

Brand X is definitely clearer and more expressive than Go here. In particular, I don't like Go's use of for for while loops... the clue is in the name!

But wait! There's more. Remember how I said that Brand X allowed ; in expressions? This is perfectly valid:

while   i := calculate_i;   print(i);   i < 0 do   dosomething od

In fact, the scoping is such that you can declare variables inside these expressions, leading to the very convenient idiom:

if   int errorcode = dosomething;   errorcode /= 0 then   reporterror(errorcode) fi

(Brand X's /= operator is equivalent to Go and C's != operator.)

You can do the reverse, too, embedded if statements into expressions:

print(if (c > 0) then "X is positive" else "X is not positive" fi)

...and because this is messy, there's an alternative syntax for this:

print((c > 0 | "X is positive" | "X is not positive"))

Some people might consider this too close to C's ?: operator for comfort, but there are situations where this sort of thing can genuinely be useful...

Go also supports limited support for for loop iterators, making it easy to iterate over various forms of built-in data structure; you can't define your own iterator. Brand X doesn't have these. Go also supports break and continue keywords for exiting loops early. Brand X also omits these; you have to use goto instead...

Go Brand X
for i := 0; i < 100000; i++ {   if (i == 1234)     break; } for i from 0 to 99999 do   if (i = 1234) then     break   fi do break:

Brand X does have a goto keyword --- but it lets you omit it! The compiler enforces safe jumping and won't let you reinitialise or access an uninitialised variable.

Switch

Both languages support the switch construct, although in different ways:

Go Brand X
switch (i) {   case 0: print("zero");   case 1: print("one");   case 2: print("two");   default: print("many"); } case i in   print("zero"),   print("one"),   print("two")   out print("many") esac

Brand X does not label the case statements; the choices are implicitly numbered incrementing from zero, which is a major limitation. If the programmer wishes to select against a non-linear of values, then it must be broken up into subranges manually, or else an if...elif chain used.

Go also provides the following form of switch:

switch {   case (i == 0): print("zero");   case (i == 4) && capitalised: print("FOUR");   case (i == 4): print("four");   case (i > 10): print("a large number");   default: print("something else"); }

In this form, the expressions attached to the case statements are evaluated top-down and the first matching statement executed --- it is equivalent to anif...else if chain.

In the above examples, neither language is doing fallthrough from one statement to the next; only one print() statement is being executed. This will be foreign to C programmers. Go, however, supports this:

switch {   case (i < 10): print("a small number");   _case_ (i > 100): print("a large number");   case (i > 50): print("a moderate size number, which is ");     fallthrough;   default: print(i); }

The fallthrough keyword causes execution to continue into the next case statement.

Constants

Both languages support compile-time constants, but in very different ways.

Go treats compile-time constants as separate syntactic entities to normal values. They're created using special syntax, and only numbers or strings can be used. Brand X, however, treats constants as variants of existing types, and any type can be used.

Go Brand X
const (   kilobyte = 1024;   megabyte = 1024*kilobyte;   gigabyte = 1024*megabyte;   terabyte = 1024*gigabyte; ) int kilobyte = 1024; int megabyte = 1024*kilobyte; int gigabyte = 1024*megabyte; int terabyte = 1024*gigabyte

(For those of you without eagle eyes, the only syntactic difference in Brand X is that the variables are initialised with= instead of :=.)

Brand X allows any variable, anywhere, to be declared immutable. This encourages the programmer to use them much more often. It's actually less typing to make something immutable than it is to make something mutable! When writing in Brand X, it's a good habit to make all variables immutable unless they need to be mutable. This not only assists the compiler in producing optimal code, but also makes the program much more resilient to errors. It also simplifies the semantics considerably: are function parameters in Brand X pass-by-value or pass-by-reference? Who cares? They're immutable. We can safely leave it up to the compiler to pass them in the most efficient manner.

Conversely, Go's constants feel very much like a thin veneer on top of the language, and don't seem much better than C's much reviled preprocessor. In fact, Go does not appear to support any form of constant type, which is an odd omission --- even C has this.

Variables, structures and pointers

Both languages support scalar variables in similar fashions.

Go Brand X
var i int = 0; print(i); int i := 0; print(i);

Both languages support user defined types and structures (although Brand X calls types 'modes' instead).

Go Brand X
type location struct {   x float;   y float;   z float; } var here location; here.x = 0; here.y = 1; here.z = 2; mode location = struct (   real x;   real y;   real z; ); location here; x of here := 0; y of here := 1; z of here := 2

The biggest difference here is that Brand X uses the of operator to access field members, rather than the . operator. While of does allow English-like syntax like width of object, I do have to admit that I prefer the traditional object.width.

The two languages diverge considerably, however, when it comes to pointers.

Go has C-like pointers, where a pointer is a specific type of entity that refers to an object elsewhere. You can only perform pointer-specific operations on pointers, and they have to be explicitly dereferenced to get the value.

Brand X has references. A reference is a modification of the entity that causes the entity's data to be stored elsewhere. You can (mostly) perform any operation on the reference that you could on the actual value; the compiler handles dereferencing them for you.

Here's an example:

Go Brand X
var i int = 4; var ip *int = &i; *ip = 5; int i := 4; ref int ip = i; ip := 5

This may seem like a fairly minor difference, but it changes the entire philosophy of coding for the language. I no longer have to explicitly dereference anything (with a few minor exceptions). What's more, the Brand X compiler will implicitly convert to and from reference types whereever necessary. For example:

Go Brand X
func swap (i *int, j *int) {   var k int = *i;   *i = *j;   *j = k; } func main() {   var i int = 1;   var j int = 2;   swap(&i, &j); } proc swap = (ref int i, ref int j) void: (   int k = i;   i := j;   j := k; ) proc main = void: (   int i := 1;   int j := 2;   swap(i, j) )

Go makes me do all the work explicitly: the & and * operators work as they do in C. Brand X, however, is doing it all automatically. It allows me to consider the problem at a much higher level. In this case, the use of references very cleanly allows swap() to have pass-by-reference parameters.

Both languages support mutable pointers:

Go Brand X
var evenspell int = 1; var oddspell int = 2; var current *int; func changemode(i int) {   if ((i % 2) == 0)   {     current = &evenspell;   }   else   {     current = &oddspell;   } } int evenspell := 1; int oddspell := 2; ref int current; proc changemode = (int i) void: (   if ((i % 2) = 0) then     current := evenspell   else     current := oddspell   fi )

But wait! you say. Wasn't Brand X using := to mutate the value pointed at earlier? And here I'm using := to mutate the pointer itself? Yes, that's true. Since I have not defined the reference to be immutable, := now mutates the reference itself. This is not an area of Brand X I'm particularly happy with: it's unintuitive and feels muddled. If I actually want to change the thing being pointed at, I need to use a cast and do:

ref int(current) := 9

...which just feels wrong. In particular, Brand X's mutable pointer semantics mean that := is no longer symmetrical:

int i; i := current; # deference current, copy value to i # current := i; # change current to point at i #

This looks to me like a bug waiting to happen... but my programming skills are steeped in C and manually-dereferenced pointers. I'm well aware that my knowledge of Brand X's type system and reference semantics is minimal at best. It's probably all perfectly logical, once you know how it works, but I don't.

Brand X also has a few odd gaps. It's possible to create mutable and immutable values, and mutable and immutable references to mutable values, you can't create references to immutable values. The type system doesn't support the 'reference to immutable thing' (a.k.a. 'pointer to const'): it's unable to forbid referencing the reference and changing the value, so violating the immutability of the thing being referenced.

Go's pointer semantics are simple. By forcing the programmer to do everything manually you can at least be sure that the programmer is doing what they intended. On the other hand Brand X's pointers are much more flexible and allow a richer range of expression.

I also have an issue with Go's syntax here. The pointer syntax, stolen from C, is very clunky. For example, this expression:

foo*bar

Is that a parameter declaration or an expression? The answer is that you can't tell, without knowing what bar is:

/* bar is a type */ func fnord(foo *bar) { ... }

/* bar is a variable */ print(foo * bar);

That's poor design. It may be syntactically unambiguous, but I'm not a parser, and I like having visual cues to make the code more readable: I find Brand X's ref int to be much more obvious --- and I grew up with C, and am used to the use of * to denote pointers.

Closures

Both languages allow functions to be nested, with proper lexical scoping:

Go Brand X
func test() {   var i int = 0;   count := func()   {     i = i + 1;     print(i);   };   for j := 0; j < 10; j++   {     count();   } } proc test = void: (   int i := 0;   proc count = void:   (     i := i + 1;     print(i)   );   to 10 do     count   od );

Go supports fullclosures, where nested functions can refer to variables in the surrounding scope, even when the scope has terminated; in Brand X it's undefined whether procedures can be used outside of their defining scope (some versions do, some don't). Go doesn't allow you to use the normal func name syntax when defining a nested function, which is an odd asymmetry.

Object orientation

Neither language is object oriented. Both languages have features that fill some of the same ecological niche and can be used to build OO systems.

Go's version is a rather elegant concept called interfaces. The language allows methods to be defined on any type; these are really just polymorphic functions with a little syntactic sugar. However, it is possible to name a set of methods, and then use that name as a type; any type which fulfills the interface can be used.

Brand X doesn't have interfaces, but it does have polymorphic methods... sort of. Actually, what it provides are user defined polymorphic operators; ordinary procedures aren't polymorphic. It's possible to abuse these in a way to achieve much the same effect as Go.

Let's have an example:

Go Brand X
type place interface {   fire_icbm_at(); }; type place_implementation struct {   x float;   y float; }; func (self *place_implementation) fire_icbm_at() {   print("Boom!"); } func make_place(x float, y float) place {   var self *place_implementation = new(place_implementation);   self.x = x;   self.y = y;   return self; } ... {   var p place = make_place(0, 0);   p.fire_icbm_at(); } mode place = struct (   real x,   real y ); proc make_place = (real x, real y) ref place: (   ref place self = heap place;   x of self := x;   y of self := y;   self ); op fire_icbm_at = (ref place self) void: (   print("Boom!") ); ... ref place p = make_place(0, 0); fire_icbm_at p

In fact, the Brand X code here is a hack. Brand X's operators are limited to two operands, which means that trying to perform any action that uses more parameters involves doing horrible stuff like passing parameters in arrays.

Brand X's operator definition syntax isn't great, as it wasn't designed to do this, but Go's method definition syntax is much worse. A method that takes multiple parameters and returns multiple values ends up having three visually identical blocks of parameter definitions, with the method name itself buried deep in the middle.

However, Go's big saving grace is the ability to name interfaces. Note that I'm actually defining my methods on *place_implementation, and yet returning a place from the make_place() function; interface instances don't have to be pointers, they can be anything. This very nicely provides separation between the implementation of the interface (in this example, *place_implementation) and the exported interface (place). I can define methods on integers, pointers, structs, arrays, you name it, and pass them around as interface instances in a very natural manner. It's also got syntactic sugar to allow two structs implemented different interfaces to be combined in a way that you end up with a struct implementing both interfaces, which is a very convenient way of doing inheritance.

One minor oddity with Go is that as it is a single-pass compiler, the compiler won't let me cast the place_implementation* into a place until after it's seen the method implementations --- because until then it doesn't know that the two types are compatible. This necessitates putting the constructor after the methods.

Union types

Both languages support union types; that is, types that can contain one of several different types of value. Brand X has a traditional union construct, but Go uses a property of interfaces to achieve the same effect. They are both run-time tagged, and the value can be extracted in a type-safe manner using a variant of the switch syntax.

Go Brand X
package main /* we define three different types, with the same  * named method defined on each */ type thing_with_int struct { i int; }; func (self thing_with_int) MarkerMethod() {} type thing_with_float struct { f float; }; func (self thing_with_float) MarkerMethod() {} type thing_with_string struct { s string; } func (self thing_with_string) MarkerMethod() {} /* this interface is now implemented by each structure */ type thing interface { MarkerMethod(); } func printthing(t thing) {   switch v := t.(type)   {     case thing_with_int: /* v is a thing_with_int */       print(v.i);     case thing_with_float: /* v is a thing_with_int */       print(v.f);     case thing_with_string: /* v is a thing_with_int */       print(v.s);   } } if roman {   printthing(thing_with_string{"MM"}); } else {   printthing(thing_with_int{2000}); } mode thing = union (   int i;   real r;   string s; ); proc printthing = (ref thing t) void: (   case t in     (int i): print(("int: ", i)),     (real r): print(("real: ", r)),     (string s): print(("string: ", s))   esac ); thing t = (roman | "MM" 2000); printthing(t)

The Go code here is excruciatingly unidiomatic. Types only implement an interface if they implement all the methods in the interface, therefore I have had to add a dummy method to my interface simply in order to create it. Rather than doing the run-time test as described here, it would be far easier just to move the logic inside printthing() into the method itself.

Nevertheless, the runtime test is possible, and demonstrates some of the interesting things that can be done with interfaces in Go. This is particularly useful when dealing with the special empty interface, interface{}; all types implement this, therefore a variable of that type can contain a reference to any type. Brand X cannot do this.

Arrays

Both languages pride themselves on having good support for arrays. They each allow arrays to be created and then bounded segments of the array to be extracted and passed around. They even use the same syntax.

Go Brand X
func fill_int_array(array []int, value int) {   for i := 0; i < len(array); i++   {     array[i] = value;   } } ... {   var buffer [256]int;   /* fill entire array */   fill_int_array(buffer[0:255], 42);   /* fill partial array */   fill_int_array(buffer[10:20], 0); } proc fill_int_array = (ref []int array, int value) void: (   for i from lwb array to upb array do     array[i] := value   od ); [256]int buffer; # fill entire array # fill_int_array(buffer, 42); # fill partial array # fill_int_array(buffer[10:20], 0)

Some important notes here: the first parameter to fill_int_array() in Brand X is a ref; in Go you'd expect it to be a pointer. It's not. In fact, Go treats boundless array types specially. They're called array slices and act like pointers. Brand X doesn't treat them specially, so I need to pass in a reference.

Go won't implicitly cast an array into an array slice, requiring the full-array slice in the above example; in addition, Go requires me to specify both bounds of the slice, even if they're at the end of the array; Brand X use sensible default values if omitted, allowing syntax such as array[10:] to slice from the tenth item to the end of the array.

Go uses the len() pseudofunction to return the length of an array (or array slice). Brand X has the upb and lwb operators. Yes, Brand X supports arrays that don't start from 0; very useful. Go also provides the cap() pseudofunction for array slices. This returns the length of the array that's backing the array slice. Go allows the array slice to be increased in size up to the size of the underlying array.

I think this is a really bad idea. If I pass an array slice into a third-party library, I do not want the library to write to my array outside the bounds of the slice I gave it! I'm trying to imagine what this is actually useful for, but haven't come up with anything yet...

Go supports single-dimension arrays only; Brand X, however, support multidimensional arrays. Go does multidimensional arrays by creating arrays of arrays. (Although you can have arrays of arrays if you like.)

Go Brand X
var screen [320][256]pixel; for y := 0; y < 256; y++ {   for x := 0; x < 320; x++   {     screen[x][y] = 0;   } } [0:320, 0:256]pixel screen; for y from 0 to 256 do   for x from 0 to 320 do     screen[x, y] := 0   od od

Unlike Go, however, Brand X allows you to do slicing on multidimensional arrays.

[0:320, 0:256]pixel screen;

proc clear = (ref [,]pixel area) void: (   for y from 2 lwb area to 2 upb area do     for x from 1 lwb area to 1 upb area do       area[x, y] := 0     od   od )

ref [,]pixel rectangle = screen[100:200, 0:8]; clear(rectangle)

(1 lwb returns a bound of a specific dimension of the array.) I can hear the graphics programmers drooling already...

Incidentally, Brand X doesn't require array bounds to be compile-time constants. Even when you're defining a new type.

proc printsequence = (int max) void: (   mode array = [1:max]int;   array a;

  for i from lwb a to upb a do     a[i] := i   od

  print(a) )

Heap storage

One of the aspects of being a garbage collected language is that you will generate garbage, which means having a heap.

Go has two pseudofunctions, new() and make(), which create new objects off the heap. User-defined types are created with new() and built-in types with make(); this distinction seems confusing and needless.

Brand X has a heap keyword that is similar to Go's new(), allocating a block of heap memory.

Go Brand X
type point struct {   x float;   y float }; var here *point = new(point); here.x = 1; here.y = 2; mode point = struct (   real x,   real y ); ref point here = heap point; x of here := 1; y of here := 2

Nothing particularly exciting here.

Neither language has constructors. However, both languages do allow composite literals, in slightly different forms:

Go Brand X
/* allocates and initialises a new point in one operation; */ var here *point = &point{1, 2} # allocation and initialisation in two operations # ref point here = heap point; here := (1, 2); # ...which can be combined # ref point here = heap point := (1, 2)

Both languages are rather disappointing in this respect --- there's no way of running code in a constructor, for example, without explicitly defining a factory function.

As an aside: Brand X actually has a loc keyword as well as a heap keyword. loc defines space on the stack. In fact, this:

int i

...is actually syntactic sugar for:

ref int i = loc int

That is, we're creating an immutable reference to a int stored on the stack.

This gives a pleasing symmetry to stack operations and heap operations. It also allows the use of this idiom:

op init = (ref point self) ref point: (   x of self := 1;   y of self := 2;   self );

ref point onstack = init loc point; ref point onheap = init heap point

Concurrency

Both languages have builtin support for concurrency and synchronisation.

Go has lightweight non-preemptive threading, which it calls_goroutines_ . These are launched by using thego keyword. These goroutines can then communicate with each other via shared memory orCSP ; the language has built-in support for channels.

Brand X also has threading, but the mechanism is unspecified. The version I'm using uses Posix threads. Tasks are launched using apar() pseudofunction which evaluates all of its parameters in parallel. (Occam users will find this familiar.) Brand X does not have channels, but it does have built-in support for semaphores.

Go Brand X
var channel chan int = make(chan int); func producer() {   var i int = 0;      for   {     channel <- i;     i = i + 1;   } } func consumer() {   for   {     var i int = <- channel;     print(i)   } } func main() {   go producer();   go consumer(); } # I'm assuming that there's a channel library loaded. # # Brand X's operator overloading syntax doesn't allow me to # # define an operator called <-, so I'm using Occam-like # # ! and ? instead. # chan channel; proc producer = void: (   int i := 0;   do     channel ! i;     i := i + 1   od ); proc consumer = void: (   do     int i = ? channel;     print(i)   od ); par(producer, consumer)

Odds and ends

Both languages support features that cannot be compared easily with the other language. Here's an incomplete list of some of the more interesting ones.

Go has the defer keyword. This is attached to a statement to cause the statement to be executed at the end of the function, after the return parameters are defined. It's a godsend for easy cleanup:

var fd filedescriptor = open("foo"); defer close(fd);

Go has associative arrays as a builtin type. You can use any type that's comparable as a key; this includes pointers, interface instances, strings, etc.

var numbers map[string]int; numbers["One"] = 1; numbers["Two"] = 2; numbers["Three"] = 3; print(numbers["Four"]);

Brand X is a multipass language --- I can declare stuff after I use it. This is a fantastically useful feature that I haven't seen on any other Algol-descended language. No more forward declarations! The ability to arrange your program the way that makes it clearest to use, not the way the compiler wants it! And mutually recursive functions become easy:

proc function1 = (int i) int: (   if (i < 0) then     function2(i)   else     i   fi )

proc function2 = (int i) int: (   if ((i % 2) = 0) then     function1(i)   else     i   fi )

Go has the ability to simulate enumerated types by using a special autoincrementing 'constant' called iota:

const (   zero = iota; /* iota starts as zero /   one = iota; / ...and is incremented every semicolon /   two; / the last expression is repeated if you omit it */   three; )

Brand X's jump labels can be implicitly cast into parameterless procedures, so allowing this kind of idiom. Yes, you can cleanly jump out of procedures.

proc openfile = (ref string filename, proc void onerror) int: (   int fd = open(filename, "r");   if (fd = -1) then     onerror   fi;   fd );

openfile("missing", abort); ... abort: print("Error!")

Go has a 'short variable declaration form', where you can omit most of the var statement when definining a new variable:

/* either */ var a int = 3;

/* or */ a := 3;

And the winner is...

So, how does Go compare with our Brand X language?

The answer is that they're both very, very similar, with much the same level of functionality. They overlap more than they differ.

Go has more in the way of actual_features_ --- interfaces, channels, maps,defer ,fallthough , proper closures --- but Brand X is more_consistent_ with its features. It's much more orthogonal, with very few special cases. Using a definition before it's defined_just works_ . Declaring a array type with a bound defined in a variable_just works_ . Placing statements where you'd expect to have an expression, or an expression where you'd expect to have a statement_just works_ . Assigning to an array slice_just works_ .

Go, on the other hand, is full of corner cases. The iota keyword, for example, only works inside a const block --- it's syntactically invalid anywhere else. Why waste a whole keyword on it, then? Why have support for iterating the builtin types without also having support for iterating user types? Why distinguish between new() and make()? Why are len() and cap() builtin pseudofunctions (occupying valuable namespace) rather than being methods on a Collection interface? What is cap() for, anyway? Why are there five different kinds of pointer? Why does dereferencing a map return two values?

Most killingly of all, it's full of examples where they did not consult the literature. const and iota is a good example. const is a crude hack to produce compile-time constants. iota is a crude hack on top of this to help with producing sequences of related values. (It looks like it was borrowed from Limbo, but Limbo's version is vastly cleaner.) Where are enumerated types? Where are constant types? For heaven's sake, where are_types_? Go's const can be used only to produce trivial compile-time constants that are integers, floats, characters, or strings. If you want anything else, including structures, you have to put them in variables. Even C can do better than that.

Brand X doesn't have any of these issues. Oh, it's got issues all of its own --- for example, the oddities in the reference type system; no varargs functions; no proper object system; the lack of little bits of syntactic sugar like keyworded composite structure literals... but it feels complete in a way that Go doesn't. I can use Brand X to implement nearly all the features of Go. I can't use Go to implement the features of Brand X.

The only major features that Go has that Brand X doesn't are proper closures, and the interface system. And I'm afraid that's not enough.

I would rather code in Brand X than Go.

So what is Brand X?

Take a deep breath. This may shock you.

Brand X is Algol-68.

Algol-68 is a very early language dating from 1968, roughly 41 years before the announcement of Go, and predating languages like C. It was one of the infamous committee-designed languages, and was panned at the time for being large, bloaty, and difficult to compile. Initial implementations could not actually implement the full language without advances in compiler technology! The committee itself had fragmented and there was considerable infighting; it very nearly didn't come out at all, with such luminaries as Dijkstra advocating a Minority Report describing an_entirely different language_ to be included with the main language specification. The specification itself is 265 pages long and is notoriously incomprehensible; not only did it invent its own deeply strange terminology ('bus token', 'invisible production trees', 'primal environs', 'incestuous unions', 'notions', 'protonotions', 'metanotions', 'hypernotions', 'paranotions'...) but it used its own grammar formalisation (avan Wijngaarden grammar). It was only until the Revision of Algol-68, released in 1974, when the specification was completely rewritten in an attempt to make it easier to understand, that the language was considered 'finished' --- by which point it was too late.

But don't underestimate its importance. Algol-68 was incredibly influential. It begat languages such as Pascal, Oberon and Ada. C's type system was directly inspired by it, and its influence can be felt even in the latest, most fashionable procedural languages such as Java. It_was_ used for teaching, and its design shaped the minds of a generation of computer scientists who then went on to design some of the languages we use today. Even the word 'orthogonality' was first coined and used in the Algol-68 specification.

So why am I comparing Go with a language 41 years older?

Because Go is not a good language.

It's got a some nice features, such as interfaces (which I am very intrigued by, and hope I start to see elsewhere), but the bulk of the language is simply a tired rehash of the same language features that we've been using for decades. In fact, I can't spot any language feature that was invented after about 1985.

I'm not suggesting that everyone drop Go and switch to Algol-68. There's a big difference between a good programming language and an effective programming language. Just look at Java. Go has a modern compiler, proper tools, libraries, a development community, and people wanting to use it. Algol-68 has none of these. I could use Go to get proper work done; I probably couldn't in Algol-68. Go is effective in a way that Algol-68 isn't.

Algol-68 really is archaic. It has freaky typography issues that mean that keywords must be specially marked for the compiler (the bold keywords in my code examples are actually syntactic!); a case statement that is frankly embarrassing; a formatted output system, transput, that's built in to the language rather than implemented as a library; an I/O system oriented around punch cards; multiple simultaneous representations of the program including an APL-tastic one designed for publication; localised keywords, allowing you to write programs in whatever (human) language you wish; scalability issues, making it hard to write large programs...

But even so, Algol-68 remains a fundamentally_nicer_ language than Go. It's more consistent. It feels more comfortable to use. It's got a pleasant solidity to it that indicates underlying logic to all its decisions, even if that logic is rather weird by modern standards. It simply feels richer and more expressive than Go.

These days, we know a great deal more about compiler technology and type theory than we did in 1968. Algol-68 may have been large compared to the languages of the day but by modern standards, it's pleasingly slim an elegant. (Don't forget that the original K&R C came out in 1972!) It's a fraction of the size of C++, and is probably comparable to ANSI C. The technical difficulties with type analysis that made Algol-68 hard to compile efficiently are no longer a problem. It would be entirely possible to bring Algol-68 up to date --- Algol-09? Algol-NT? --- at which point it would easily rival most of the other procedural languages in use today.

And that's really depressing. In 2009 we're getting excited about a language that is less good than one that was released 41 years earlier. Have we learned nothing in the meantime? Well, yes, we have; programming theory has come on leaps and bounds, but it's largely been restricted to the functional programming world. New technology has been very slow to migrate over to the procedural programming world, which is why we're only just discovering the joys of generics and type inference.

Which brings me back to Go. There's a lot of work there. Looking at aspects other than the language semantics itself, it appears well-written and effective using modern engineering techniques. This is a good thing. Writing compilers is hard, and the Go developers have put a lot of effort into it.

The Go team is obviously well-funded, has plenty of expertise with excellent pedigrees, and has been assigned the task of producing a useful language. This would have been the perfect opportunity to go and find all those new techniques and turn them into a well-designed, orthogonal, expressive, and useful programming language! But instead they fluffed it, and what we have is Go.

Imagine what they could have done if they'd put all that effort into modernising Algol-68!

You can visit Go's web site, where you canget a copy of the compiler,read the tutorial,examine the language specification, or join the mailing list.

If you want to know more about Algol-68, theWikipedia page is a good place to start. You probably don't want to readthe formal language specification as it's unreadable. You're much better off withAndy Tanenbaum's tutorial. A denser and more complete work is Sian Leitch's_Programming Algol-68 made easy_. In terms of implementations, the best one I've found so far (and the one I syntax checked the examples on this page against) is theAlgol-68 Genie interpreter; not only does it implement most of the language, butthe excellent manual contains a good tutorial, as well as a formal definition of the language that's actually readable. If you want a real compiler, you're probably out of luck --- the only remaining working compilers are commercial. However, there's aAlgol-68RS to C converter that generates reasonable code. The ultimate Algol-68 resource (read: list of unorganised links) is the Open Directory Project page.

If you're of a historical bent, there's afascinating document on the making of Algol-68 written by one of the committee members. You can also learn aboutAlgol-68's considerable influence in the USSR.

Bugs?

Spot any errors in this document? Please let me know! You can use the comment form below or email me directly. I'd be the first to admit that I'm not an experienced programmer in either Algol-68 or Go. I only heard about Go five days ago...