I’ve been being peppered with questions about Go, the new programming
language just released as open-source by Google. Yes, I know about it. And
yes, I’ve used it. And yes, I’ve got some strong opinions about it.
Go is an interesting language. I think that there are many
fantastic things about it. I also think that there are some really dreadful
things about it.
A warning before I go on: this post is definitely a bit of a rush job. I wanted to get
something out before my mailbox explodes
. I’ll probably try to do a couple of
more polished posts about Go later. But this should give you a first taste.
The natural question is, what does it look like? It’s vaguely C-like, but
with a lot of cleanups and simplifications. Every declaration is preceded by a
keyword that identifies what it’s declaring: type,
func, var, or const. Lots of parens
that are required in C have been removed. The type declarations have been
cleaned up quite a lot – they’ve gotten rid of the type-declaration garbage of
C.
In every declaration, the name comes first, followed by the type. So, for
example, types are declared after variable names, and all type
modifiers precede the types. So *X is a pointer to an
X; [3]X is an array of three X‘s. The
types are therefore really easy to read just read out the names of the type
modifiers: [] declares something called an array slice; “*”
declares a pointer; [size] declares an array. So
[]*[3]*int is an array slice of pointers to arrays of three
pointers to ints.
Functions in Go are amazing. They start off really simply, but by
providing a few simple extensions, they let you do all sorts of things.
To start off, here’s a factorial function in Go.
func Factorial(x int) int {
if x == 0 {
return 1;
} else {
return x * Factorial(x - 1);
}
}
Go extends that by adding support for named return values. You can declare the
return value as a variable in the function header; then you can assign values
to that variable. When the function returns, the last value assigned to the
return variable is the return value. So you could also write factorial
as:
func Factorial(x int) (result int) {
if x == 0 {
result = 1;
} else {
result = x * Factorial(x - 1);
}
return;
}
You can also write a function with multiple return values:
func fib(n) (val int, pos int) {
if n == 0 {
val = 1;
pos = 0;
} else if n == 1 {
val = 1;
pos = 1;
} else {
v1, _ := fib(n-1);
v2,_ := fib(n-2);
val = v1 + v2;
pos = n;
}
return;
}
(The above contained boneheaded mistake 1: I just wrote that out, and didn’t bother to compile it. Naturally, I screwed it up in a silly way. It’s since been fixed.)
It’s not object-oriented in a traditional way; it provides something like
a limited form of object-orientation using another extension to functions. You
can define new types – either type aliases, or structure types. You can
declare methods for any type at all in the module where it’s defined.
A method is just a function which has a parameter preceeding the
function name.
So, for example, a basic linked list could be implemented as;
type IntList struct {
next *IntList;
val int;
}
func (s *IntList) SetNext(n *IntList) {
s.next = n;
}
func (s *IntList) GetNext() *IntList {
return s.next;
}
func (s *IntList) SetValue(v int) {
s.val = v;
}
func (s *IntList) GetValue() int {
return s.val;
}
(Boneheaded mistake 3: I originally put the declarations in the wrong
order above – even within a struct, it’s name-first – so “next *IntList”
not “IntList* next“.)
One of the dominant themes that you’ll see as I continue to describe it is
minimalism. The guys who designed Go were very focused on keeping things as
small and simple as possible. When you look at it in contrast to a language
like C++, it’s absolutely striking. Go is very small, and very simple. There’s
no cruft. No redundancy. Everything has been pared down. But for the most
part, they give you what you need. If you want a C-like language with some
basic object-oriented features and garbage collection, Go is about as
simple as you could realistically hope to get.
To give you one example of that minimalist approach, Go allows you to
define types with methods. But there’s no such thing as a class, and there’s
absolutely no inheritance. Instead, there’s a kind of composition. I won’t go
into detail about it here.
As I said above, you can define methods on any type. The methods
are really just functions with a special parameter. There’s no such thing as a
type constructor! There’s an allocation operator, “new” – but it doesn’t
initialize values. You can’t provide Go with any automatic initializer.
Instead, things like constructors are handled by convention. You’ll generally
create a new data type inside of a module. That module will have a public
function named “New”, which returns an initialized value. So, for example,
there’s a module named “vector” containing an implementation of a vector; to
create a vector, you import the vector module, and call
vector.New(size).
Another example of that minimalism is the way that they handle abstraction
and name hiding. There are exactly two kinds of name visibility: public, and
private. Private things can only be seen in the module that declared them;
public things can be seen in any module that imports them. The way that you
make things public is by lexical cues: public things are things that were
declared with an identifier starting with an upper case letter. “fact” is
private, “Fact” is public.
The most innovative thing about it is its type system. There are two kinds
of types in Go: concrete types, and interface types. Concrete types are
exactly what you’re used to from most programming languages. Interface
types are similar to interface types in languages like Java, with one
huge exception: you don’t need to declare what interface types you
implement! An interface is a specification of what methods a type must
provide to be used in some context. Anything which implements
those methods implements the interface. Even if the interface was
defined later than a type, in a different module, compiled separately,
if the object implements the methods named in the interface,
then it implements the interface.
To make that even better, methods aren’t limited to objects. In fact, Go
doesn’t really have objects. Any value, any type at all, can have methods. So
you can make an integer type with its own methods. For
example:
type Foo int;
func (self Foo) Emit() {
fmt.Printf("%v", self);
}
type Emitter interface {
Emit();
}
Foo implements Emitter. I’ve created the type
“Foo”. Values of type “Foo” are integers. Then I’ve implemented a method on an
instance of “Foo”.
That’s brilliant. I absolutely love it. And even in the
relatively small amount of time I’ve spent hacking Go code, I’ve
seen it pay off, when I’ve created new interfaces, and realized that
old types already implement the interface. It’s a very elegant idea,
and it works really well. It ends up giving you something with the
flavor of Python-ish duck typing, but with full type-checking
from the compiler.
For example, the first thing I wrote in Go was a parser combinator
library. I defined an interface for the input to a parser:
// ParserInput represents an input source readable by a
// parser.
type ParserInput interface {
// Get the character at an arbitrary position in the
// input source.
CharAt(i int) uint8;
// Get the number of characters in the input source.
Size() int;
}
Now, anything that implements CharAt(int) and
Size() can be used as an input to a parser. Then,
I defined an interface for parsers:
// ParseValue represents the type of values returned by
// successful parses.
type ParseValue interface { }
// A Parser is an object which parses input sources. The
// framework of parser combinators provides a very general,
// backtracking parser.
type Parser interface {
// Run the parser on an input source, starting with
// the character at a specified position. If the parse
// succeeds, it returns "true" as the status, the
// number of characters matched by the parser as the match_len,
// and an arbitrary parser-specified, return value as the result.
// If the parse fails, then it returns false as the status, -1 as
// match_len, and nil as the result.
Parse(in ParserInput, pos int)
(status bool, match_len int, result ParseValue);
}
So a parser is anything which has a Parse method. A parse
method takes an input, and a position in the input; and it returns three
values: a success code (indicating whether the parse succeeded or failed); a
match length (indicating how many characters in the input were accepted by the
parser), and a return value. The return value can be anything: it’s
defined as an empty interface, and everything implements the empty
interface.
To build parsers, you start with a kind of parser that can process
a single input character, which is any character from some set of
characters.
// CharSetParser parses a single character from among a
// specified set.
type CharSetParser struct {
chars string;
}
func (self *CharSetParser) Parse(in ParserInput, pos int)
(status bool, match_len int, result ParseValue) {
status = false;
match_len = -1;
for c := range self.chars {
if self.chars[c] == in.CharAt(pos) {
result = self.chars[c];
match_len = 1;
status = true;
}
}
return;
}
This demonstrates one sort of odd feature of Go. A structure and a pointer
to a structure are different types, and they can have different
methods. A pointer to a
CharSetParser
is a parser;
but a
CharSetParser
is not!
The implementation above doesn’t provide a way of creating a
CharSetParser
. To do that, we need a function. And to try
to make the parsers look as clean as possible, we’ll name it
so that it doesn’t look like a creator function, but just a parser itself:
// Create a CharSetParser which accepts any one character
// from a specified string.
func CharSet(s string) *CharSetParser {
return &CharSetParser{ s };
}
Then you add more kinds of parsers which allow you
to combine simple parsers. For example, one combinator
is repetition, which runs a parser over and over until it succeeds:
// ManyParser is a parser that parses a repeated syntax
// element. It succeeds if the sub-parser succeeds at least
// a specified minimum number of times. Returns a list
// of the results of the sub-parses.
type ManyParser struct {
min int;
parser Parser;
}
// Create a ManyParser which matches min or more
// repetitions of the sequence parsed by p.
func Many(p Parser, min int) *ManyParser {
result := &ManyParser{ min, p };
return result;
}
func (self *ManyParser) Parse(in ParserInput, pos int)
(status bool, match_len int, results ParseValue) {
status = false;
curPos := pos;
numMatches := 0;
stepResults := vector.New(0);
stepSuccess, stepLen, stepResult := self.parser.Parse(in, curPos);
for stepSuccess {
numMatches++;
curPos = curPos + stepLen;
stepResults.Push(stepResult);
stepSuccess, stepLen, stepResult = self.parser.Parse(in, curPos);
}
if numMatches < self.min {
stepSuccess = false;
match_len = -1;
results = nil;
return;
}
status = true;
results = stepResults;
match_len = curPos - pos;
return;
}
With that, you just say Many(P, 0) to represent a parser
that parses 0 or more repetitions of P.
Anyway, the end result is a library where you can write lots of
very simple functions which combine parsers in very flexible ways.
The end result is something where I can write a parser that parses
lisp SExpressions into a cons-list structure with:
SexprParserRef := MakeRef();
ManySexprs := Action(Many(SexprParserRef, 1), new(VectAction));
ListParser := Second(Seq([]Parser { Lp, ManySexprs, Rp }));
Sexpr := Alt([]Parser{ ListParser, SymParser });
SexprParserRef.SetTarget(Sexpr);
Alas, not everything about it is so wonderful. Sometimes that minimalism
comes back to bite you on your ass. And worse, there’s a bit of a “for me but
not for thee” attitude that pervades some aspects of the language design.
For one example of an odd minimalist tradeoff: they decided that they
didn’t want to specifically add something like an enumeration type. After all,
what’s an enumeration? It’s really a type-safe alias for an integer, with a
set of constants defining the members of the enumeration. Go already has the
ability to define a type-safe alias for an integer! So why bother allowing the
definition of a new kind of type? Instead, just make it easy to define the
constants. So they created a pseudo-variable named iota which can
be used inside of a constant declaration block. Each time you see a semicolon
in a constant block, iota is automatically incremented. So to define a set of
colors, you could do something like:
type Color int; const ( RED Color = iota; ORANGE = iota; YELLOW = iota; GREEN = iota; BLUE = iota; INDIGO = iota; VIOLET = iota; )
That will create a color type, with “RED” as 0, “ORANGE” as 1, etc. Of course, it’s kind of annoying to have to re-type the iota every time. So
if you omit the values after an iota in a const block, it automatically
just copies them. So you could rewrite the colors as:
type Color int; const ( RED = iota; ORANGE; YELLOW; GREEN; BLUE; INDIGO; VIOLET; )
It seems a bit strange and obtuse, but not awful. But it can get downright
strange. Here’s an example from the Go tutorial:
type ByteSize float64 const ( _ = iota; // ignore first value by assigning to blank identifier KB ByteSize = 1<<(10*iota); MB; GB; TB; PB; YB; )
Iota starts at 0. By assigning it to “_”, you effectively discard the zero
value. Then, “KB” is defined as 210*iota. That full expression is
then copied to the successive values. So MB gets a copy of Bytesize =, which evaluates to 1<<(10*2). And so on.
1<<(10*iota)
Since ByteSize is an alias for a float64, the
values are automatically converted to floats.
It’s powerful, but if you ask me, it’s ad-hoc and ugly.
Then there’s the “We’re the language designers, we need stuff you don’t”.
The Go type system does not support generic types. They’re
considering adding them at some point in the future, but for now,
they don’t consider them necessary. Lowly programmers just don’t
need parametrics, and it would clutter up the beautiful compiler
to implement them.
Oh, but wait… Go really needs type-safe arrays. Well, that’s OK.
Everyone provides arrays as a sort of special case – what language doesn’t have
typed arrays, even if it doesn’t have any other parametric types? And we really
want these cool things called slices – but they really need to be strongly typed.
So we’ll let them be parametric. And maps – we really need a map type,
which maps keys to values, and it really needs to be type-safe. So we’ll add
a parametric map type to the language, by making it a special case built-in.
So: you can’t write parametric types – but they can. And that
creates a very weird asymmetry to the language. Everything in Go is passed by
value – except for the built-in slice and map types, which are passed
by reference. Everything is allocated by “new” – except for the
built-in slice and map types, which are allocated by “make”. It’s
by far the biggest blemish in Go, and it’s absolutely infuriating.
have exceptions. I can accept that – exception handling in most languages
is absolutely hideous, and you can make a strong argument that
it’s so damned broken that you should find a way to do without. Particularly
in a language like Go, where you can return multiple values from functions,
you can do things like return both a result and a status code, and handle
errors via status codes.
So the Go guys left out exceptions. They say that you don’t need them. But
for their own code, they added workarounds that become something like
exception handling for their own code. For example, if you want to do a cast
from an interface type X to an interface type Y, you’d write something
interface type to another, you’d write “y = x.(Y)“. But if the
value x didn’t implement interface Y, it would be an error, and
the program would crash. So if you’re not sure that x implements
Y, you can write “y, ok := x.(Y)“. That never fails;
if x implements Y, then y gets the casted value, and ok is true.
If x doesn’t implement Y, then y get assigned nil, and ok is set
to false. Once again, having a way of catching an error is OK for the language
designers, but not for anyone else.
Anyway, with the complaints out of the way: one other major piece of
goodness is compilation speed. One of the fundamental goals of Go was to be
able to compile things really quickly. Lots of things about the
language were designed to make it possible to build a really fast compiler,
and to be able to do full separate compilation without every needing to
re-process anything. (The motivation for this is that at Google, we have a
very large code-base with tons and tons of code re-use. This is a very good
thing. But because most of that code is C++, builds can be incredibly slow.
The way that C++ header files work with a standard compiler, you can wind up
re-parsing the same file hundreds or thousands of times. So even with a really
fast compiler, you can easily wind up with some extremely slow compile times.
Tricks like pre-compiling headers can help, but they’ve got their own
problems.)
Go programs compile really astonishingly quickly. When I first tried it, I
thought that I had made a mistake building the compiler. It was just too
damned fast. I’d never seen anything quite like it. I’d taken a parser
combinator library that I’d written in Java, and re-implemented it in Go – the
full version of the code that I excerpted above. The code was slightly more
than 30% shorter in Go, and also cleaner and prettier than the Java. A full, clean
build of the combinator library in Java took just over 3 seconds in Eclipse. It took
0.6 seconds on the command-line compiled with Jikes. With the 6g go compiler,
it took 0.06 seconds!
Before using Go, Jikes was the fastest compiler I’d ever
used. But Go managed to do better by a factor of 10! And
Jikes was generating Java bytecode; 6g is generating
reasonably well-optimized native code! Part of that is
the genius of Ken Thompson, the guy who implemented 6g; but part of
it is also the very careful design of the language.
(Boneheaded mistake 2: for some reason, I always confuse Ken Thompson and Dennis Ritchie. It’s Ken who wrote 6g, not Dennis, as I originally wrote.)
So… At the end of the day, what do I think? I like Go, but I don’t love
it. If it had generics, it would definitely be my favorite of the
C/C++/C#/Java family. It’s got a very elegant simplicity to it which I really
like. The interface type system is wonderful. The overall structure of
programs and modules is excellent. But it’s got some ugliness. Some of the
ugliness is fixable, and some of it isn’t. On balance, I think it’s a really
good language, but it could have been a lot better. It’s not going to wipe C++
off the face of the earth. But I think it will establish itself as a solid
alternative. And hopefully, over time, they’ll fix some of the worst parts
of the ugliness, without sacrificing the beauty or simplicity of the language.
To preemptively answer a question I’m sure people will ask: am I using
Go for my daily work? Not yet. My project has a lot of existing code, and at the moment, I’m constrained by that. I’ll be starting work on a prototype of a major new component very soon, and once I’ve had time to look at the foreign function interface for Go, I’ll make a decision about whether or not to use it. Basically, if the FFI is good enough to allow me to link to the Google infrastructure that I need to be able to use, then I will use Go for my prototype, and hopefully for the full component.