Syntax-K

Know-How für Ihr Projekt

The Go Programming Language,
or: Why all C-like languages except one suck.

2011-06-07

by Jörg Walter <info@syntax-k.de>

Chinese translation
Serbo-Croatian translation
Change Log of this article

Update eleven years later

A short update: A lot has happened. Go has its place in the software world, but other languages developed as well. I don't write new Go code anymore, various people voice (IMHO valid) criticism about golang, C++ has a supposedly safe subset these days, Rust is a completely new language with strong guarantees if you can navigate its complex semantics, just to name a few. So take the below with a grain of salt. I still prefer using Go software over random C/C++ programs, and Go still has arguments in its favour, but it is no longer the only contender for a safe, fast, and comfortable systems programming language.

I personally have grown to dislike its (intentional) verbosity. Hiding complexity can be good for maintainability and making the logic of your code stand out more clearly. C++ improved hugely in the meantime, but its legacy means you still need to know how to not shoot yourself in the foot. Then again, Go is prone to its own class of common bugs. Given that most of my work today is in (deeply) embedded devices, Go just doesn't fit my bill anymore. I should seriously learn Rust in order to see if it is the solution some people claim it to be. Then again, the phrase "I should learn Rust" appears all too often on the web, which probably says something about it as well...

On to the original article:

Introduction

This was meant to be a review of the Go programming language developed since 2007 by Robert Griesemer, Rob Pike, and Ken Thompson at Google. By now, Ian Lance Taylor, Russ Cox and Andrew Gerrand have joined the core team. It is a C-like language with some features of dynamic scripting languages and some novel (at least in the field of general purpose languages) approaches to concurrency and object orientation. It is intended to be a systems programming language, which is why this review pits it against other C-like languages and not against scripting languages.

During the writing of this review, I noticed that many aspects of Go deserve a more detailed explanation before they can be evaluated. Go simply is different, you can't judge it from a classic OO background. So this is as much an introduction to Go as it is a review. I'm new to Go myself. Writing this helped me to understand what Go does and is, but keep in mind that I am still in the middle of my first Go application. I've coded in lots of languages, so I'll compare Go to aspects of many of them.

Go is young. It has been declared stable only this year. With this review, I also hope to contribute to the discussion about the future direction of Go, making it a truly awesome language. This includes pointing out the deficiencies that still exist.

Rant about C-oid languages

I am always interested in new programming languages. Usually, I use convenient, dynamic languages like JavaScript, Perl or lately Python. Most of the time, I prefer readability, maintainability and development efficiency over raw benchmarked speed. Premature optimization is usually not worth it. Security also matters, and scripting languages protect you from the world of buffer overflows, format string vulnerabilities and all the other low-level attacks (assuming the runtime itself isn't exploitable).

But there is a downside to those languages mentioned. They don't scale well, and my work encompasses everything from 8-bit MCUs via embedded ARM systems and smartphones through classic desktop applications. I tend to use C(++) for them, but then my expressiveness suffers a lot. No convenient string operations, clunky regexes that need external libraries, manual memory management, and of course all the security nightmares of the last 40 years. But for what it does well, it's nearly as fast and memory-efficient as you can ever get.

So there's something fundamentally lacking in the low level language space. What we use to write low level tools and operating systems is decades old and oblivious to the challenges of today's system environments. Why this is so I want to show here. It only covers languages of C descent style, since that's what people are accustomed to, but you could easily add entries about Pascal, Modula, Oberon, Smalltalk, Lisp and what else has once been at the core of a computer system.

C

I like C. I really do. Its simplicity make it quite beautiful. Unless you go about and do something stupid like writing a complex GUI toolkit with C as its primary API.

One really good thing is that you have almost assembler-like control about what happens, which is important in certain scenarios. Unless you use optimization, that is. Then the convoluted semantics of C fight back, like not clearing memory containing sensitive data or reordering statements across synchronization barriers. This can't be avoided. If you have pointers and arithmetic on them, you need non-obvious semantics in the language to make optimization not suck.

But the true downside is strings, memory management, arrays, well... about everything is prone to exploitation. And clumsy to use. So while C may be as lightweight as it can get, it's not really suitable for projects with more than 10k LOC.

C++

C++ improves on some aspects of C, but is worse on others, especially useless syntax verbosity. It has always been a bolt-on, and you notice it. It is a superior alternative for classic application development, and some nice slim and full-featured GUI toolkits use its qualities well.

But when you try to do the modern funkiness of dynamic languages (lambda functions, map/reduce, type-independence, ...), it's usually along the route of "Hey cool, this is actually possible with C++! You just need to do

dynamic_cast<MyData*>(funky_iterator<MyData &const*>(foo::iterator_type<MyData>(obj))

Yeah. Right.

Don't get me wrong, I love templates, but contemporary C++ using STL looks like a classic case of the "If all you have is a hammer"-syndrome. GCC had to implement special diagnostic simplifications just so you can actually find out that that 5-line error message was a simple const'ness mistake when using a std::string method. Even worse, they can be slow as hell. Ever waited for Boost to compile? Good idea, bad realization.

Objective C

I feel a bit heretic here. I shouldn't diss anything coming from NeXT. Still, I can't help it—the bolt-on feeling also applies to Objective C. It's not nearly as pointlessly verbose as C++, but that bracket syntax is like a parallel world entering C, just so the holy Syntax Of C is not disturbed.

You don't get to write impressive template casts as in C++ (this may be an advantage ^^), and memory management still is kind-of-manual. At least in C++ you could get reference counted pointers for free (if you happened to find the one correct template syntax among thousands of incorrect ones).

Objective C sidesteps this issue by officially offering optional GC. Wait—it always was optional for C-oids. boehm-gc exists for how many years? But standard fare are memory pools, which is a nice and worky kludge for many situations, but a kludge nonetheless.

Java

You didn't really think I would forget Java in my rant about C-oid languages, did you? Now, Java is almost the solution. Almost, were it not for reality.

We get binary platform independence, a detailed language specification with various implementations and no significant traps, classic OO, garbage collection, some truly dynamic features, lately even generics—and a prohibitive memory consumtion and slow startup times.

There's no actual need why this should be the case. Advanced JIT compilers can (in theory) optimize better than any static ahead-of-time compiler ever could. GCJ compiles down to machine code, if you want it to. The VM is even suitable for a complete hardware implementation. But the JDK sets the foundation for bloat, and many contemporary Java projects sport a byzantine complexity.

Now, you can write quite comfortably in modern Java. The web services offer some really nice abstractions. Up until you look under the hood and discover a Rube Goldberg machine. Each layer builds upon last year's favourite abstraction layer. You can't compromise backwards-compatibility, right?

Look at your average web application's lib directory. I wouldn't be suprised to see a hundred JAR files there, all just for a simple search database or shopping site which even PHP would do in 10k LOC. And should you be adventurous, try to build it yourself. A world of fun! Setting up a Linux system from scratch, without any step-by-step instructions, is easier. Trust me, I have done both. Be sure you know how to spell "dependency hell" forwards and backwards before you begin.

And all that wrapped in a way too verbose syntax and an old-school object model. There is nothing fundamentally wrong with this, but others do better. State of the art is something else. Take a look at Perl 6, which really tries to put results of modern language design into a usable (for certain values of "usable") language. And those first-ever-in-production features are still decades old! Java is nowhere near this, except maybe for generics.

C#

I almost forgot this one. I actually did forget it, until feedback reminded me of it. Frankly, I hardly know C#. As a language, it seems to be nice, a great evolution of C and C++. What makes me stay away from it is the non-free nature. Yes, there is Mono, but I wouldn't like to base my stuff on a language that is there because of Microsoft's benevolent permission which it could turn into patent-lawsuits any time. We all know the tricks that company (well, actually any large company) has up its sleeves.

I don't see the point in writing non-cross-platform code, so with Mono being too threatened for my taste, I stay away from it. Also, the CLR must first earn enough reputation, while the strengths and weaknesses of the Java VM are well-understood. I may be writing C# code some day, but it won't be any day soon.

With the lack of an open ecosystem of language tools and alternate and/or special-purpose implementations, it's simply not fit to be a systems programming language. It's a corporate-only gig.

Oh, and the fact that a company wants to earn money with the development of the language itself is not the road to a healthy evolution. Corporate interests will some day enforce choices that are bad for the quality of the language. There is a constant pressure for improvement, otherwise you can't sell anything. As a contrast, look at TeX, which is more or less the same for 30+ years, and as bug free as software can ever get. You can't really compare both, but it shows where the spectrum ends, and C# is at the wrong end.

JavaScript

JavaScript doesn't sound like it belongs here, since it is a fully dynamic scripting language. It is one of the most widely deployed programming languages, however, and it is C-oid as well. And, more importantly, JS engines these days sport quite advanced optimized JIT compilers, so performance can be in the same ballpark as with the other languages mentioned.

So, what's fundamentally wrong with JS? Not a lot. Only it is not suitable for small systems. It is a great application-embedded language, it is suitable for writing network services as well, but its design explicitly provides no way of interacting with the outside world. The hosting application defines all interaction APIs in an implementation-defined manner, so by definition it can't be the system's hosting language.

Moreover, the JIT and runtime requirements make its embeddability limited. If you own a Palm Pre, you already use JS as embedded language, and it is very convenient. Only that 500MHz/256MB system is at the low end of what's useful. Perhaps the lowest spec device using JS (or rather, ECMAScript) as its main system language is the Chumby, playing Adobe Flash Lite movies on 450MHz/64MB. Not exactly a universal language there.

Wishlist

Dear Santa, all the low-level languages suck. For Christmas I want a programming language that has the following features (with examples from established languages), in loose order of importance:

General Design Principles

1. Expressiveness
I want a language that is high-level enough that I can actually express my ideas and algorithms in, not one that wastes my time and screen space with tedious management tasks or patterns which consist of 80% boilerplate code. The more important elements of expressiveness are listed separately. A good test is writing a parser. If the resulting code has a lot of similarity to the constructs you are parsing, that's the right direction. (Example: Perl 6 grammars)
2. Simplicity
I want a language that is elegantly simple. Only a few concepts should express all possibilities. Orthogonality is a key aspect of this. Simplicity also makes the language easier to learn. Reuse syntactical constructs for similar purposes and let user code interface these constructs so they can create added value to these. Also, don't force complicated structures upon the users. There's nothing wrong with offering classes, a unit testing framework or doc-comments, but keep them out of sight if the user doesn't want them. Ideally, make them DWIM.
3. Equal Rights
If the built-in associative array algorithm is inefficient for my specific data set, I want to be able to create an alternate implementation that is used exactly like the built-in one. The language should not be privileged. Consequently, operators should be overloadable. Oh what magic have I done in Python... Also, built-in data types should be usable in the languages's object syntax. All of this need not be dynamic, most of this can be statically evaluated syntactic sugar. (Example: Perl's prototyped subs, Python's operator overloading)
4. Meta-Programming
This has two aspects. One is templates and/or generics, which are too useful to not have. Even C had some evil hack mimicking this: the preprocessor. Everyone needs this. The more advanced aspect is compile-time executed code that generates code, like Lisp's macros or Perl's source filters. Bonus points if this allows to create new language constructs or a domain-specific language.
5. Efficiency and Predictability
If a language aims at system programming, it must be fairly efficient and complexity must be predictable. I must be able to intuitively estimate in what order of magnitude time and memory demands of a certain operation lie. Core language features must be well-optimized. The library may offer high-level constructs with more coding time efficiency but less runtime efficiency. The object system must not require expensive runtime support (neither in time or memory), static optimizations should generally be possible.

Ideally, it should be possible to write a useful control application in a few kiB of code and some hundred bytes of memory. Of course, efficiency and features usually are the opposite ends of a tradeoff, so the language may give me the chance to decide.
6. Usability
At the end of the day, the language must be usable. All those ideals aside, foremost it must solve real problems. A little pragmatism doesn't hurt. The language should be sufficiently close to other languages so you can think in terms you are used to. If you deviate drastically, it must be worth it. Honour the principle of least surprise!
7. Module Library and Repository
I want all the niceties I have grown used to in scripting languages built-in or part of the standard library. A public package repository with a decent portable package manager is even better. Typical packages include internet protocols, parsing of common syntaxes, GUI, crypto, common mathematical algorithms, data processing and so on. (Example: Perl 5 CPAN)

Specific Features

8. Data Structures
I want my hashes! A language without associative arrays as well-integrated data type is a joke. OO (or some other, convenient method of encapsulation) is also a must, obviously. A boolean type is nice, but more important is a sensible interpretation of any data type in a boolean context. Fat bonus points for advanced data structures in the standard library, like various trees, lists, sets, etc. If you've got a skip list, you are on the right track.

And make strings OOish, i.e. the same operations (say, len()) should work on a string variable as on an array or an object that mimicks an array. Bonus if all primitive data types sport the same OO syntax as all other objects. No need to go Ruby though. Just make it syntactic sugar. (Example: Python and its standard library)
9. Control Structures
Sounds obvious, but decent looping constructs allow breaking out of multiple nested levels at once. Eliminate goto, really. The last time I needed that was, like... okay, I admit, it was last week. I was doing Retro-Show-Programming on a Commodore C64 at the Oldenburger Computer-Museum. That doesn't count. So scrap goto, but give me foreach. Not much needed beyond that. Exceptions perhaps, but please don't hammer each nail home with them. JavaScript's with statement I have never used, and while it is kind of a nice idea, I guess it's a case of "Less is More". (Example: all the scripting languages do fine here)

Actually, there is something I haven't seen anywhere yet. Lately, I have encountered lots of loops where loop entry and condition testing/loop exit did not occur adjacent to each other. So if there was a way to express a loop which starts at a freely chosen point in the middle, that would be oh so cool. Otherwise you will have to duplicate part of the loop. A bit like Duff's device, just not for optimization but for making the code less redundant.
10. Expression Syntax
Many people shun it, but the ?: ternary operator is a good idea, if used correctly. Python does foo if bar else baz, which is a little more verbose but still okay. Most dynamic languages, however, rock with their boolean operators AND and OR not just evaluating to true and false, but to the actual value that was considered true. Imagine the assignment value = cmdline_option || "default". That requires a decent boolean interpretation of all data types, however.
11. Functional Qualities of Expressions
If I wanted to write Lisp, I would do so. I don't need a fully functional programming language. But nothing beats a good map(). Closures and anonymous functions (lambda expressions) are a killer feature. Probably in the "too complex" area would be hyper operators (as Perl 6 calls them) like any() and all() (as Python calls them), but they rock and give the chance for implicit parallelization. Welcome to the new millennium, or at least to the 90s.
12. Objects
There are various models of object orientation out there, but the minimum requirements are encapsulation and polymorphism. Some way of composing classes should also exist, like inheritance and mixins. Interfaces should exist, or multiple inheritance as a replacement. Overloading is important, or at least give me default arguments. Mentioning arguments, named arguments are way cool.
13. Concurrency
I use this term loosely. Generators and Coroutines somehow fit into this as well. The point is not to have multi-threading built-in, but to have multiple code executions work together conveniently. They don't need to execute at the same time, but it should be possible to work on several parts of your data set at the same time. Structuring your application around event processing should be easy. If it's a mechanism for true multiprocessing, all the better. (Example: Perl5's POE, Pythons generators)
14. Strings and Unicode
It is f*cking 2011, we don't need anything else but Unicode anymore. So please have a safe string type and Unicode all over, no exceptions. I am sick of implicit conversions with accidental double-encodings and whatnot, or manual conversions with no language support. I prefer UTF-8, by the way. Who cares about constant-time indexing of strings? Use a char array for that special case. Use regexes for the common cases. Have regexes part of the regular string API!
15. Low-Level Interface
At some point, you will want to twiddle some bits manually. Especially when targeting microcontrollers or embedded ARM cores, there should be a way to get down to the bare metal. Ideally, it would be possible to write an OS kernel in the language, with no assembler code at all (except platform-specific startup code that can't be done any other way)

Enter Go

Overview

I was a bit skeptical when I read about Google's new programming language. I simply ignored the news. After all, the next New Great Language is just around the corner. Some of them enjoy a phase of hype, but then fade again, others stay in the spheres of irrelevancy, while yet others will be ready for public consumption any decade now.

Some time later I stumbled over it again. This time I took a closer look. One thing I didn't notice at first: One of the inventors is Ken Thompson of Unix and Plan9 fame, and he was indirectly involved with C as well. Now if a new programming language is designed by someone who already served in the trenches of the great mainframe era, maybe there is something to it.

So what exactly can Go do what Perl, Python and JavaScript don't give me? And what can it do what only those used to be able to do? What makes it different from all those failed or limited-success languages? Will it still be around in 30 years? And most importantly: Does it address my needs?

First Contact

The most important aspect of Go is the target audience. It was designed as a systems programming language, so it aims at lower level software, maybe even an OS kernel. Consequently, higher level constructs might be missing, as they are complex and don't map well to hardware instructions. Funny thing is, most of the convenience is actually there.

Next thing you learn is, it is a language of C descent, so load your US keyboard layout for easy access to curlies. But if you see some example source code, it looks way less C-oid than expected. Less parens, not a semicolon in sight, and few to no variable declarations, at least on first sight. This is really light on syntax, with noticeably different keywords and control structures, but still understandable.

Key Differences to C

For all those that are familiar with C/C++, let's have a quick comparison of the differences:

No semicolons!
No kidding! Well, actually there are semicolons, but they are discouraged. It works like JavaScript, there is a simple rule that makes the parser insert a semicolon at certain line ends. And that rule is really simple, so it's easy to replicate in source-processing tools. For that reason it is way less brittle than in JavaScript.
The OTBS
Next in category "pure heresy": Go defines a canonical indentation and the One True Bracing Style. And RMS is not going to be happy about it. This goes as far as supplying gofmt, a tool to routinely reformat your source. Well, Java developers are used to this, only they are stuck with a few braindead aspects (indent depth 2? SRSLY?). Source formatting in Go can be summarized like this:
  • Indent with tabs (which lets the user twiddle his editor settings to get his comfortable amount of horizontal white space)
  • Braces go on the same line as the control statement they belong to
  • Continued lines must not end in closing braces or identifiers, i.e. make the operator last on the old line, not first on the new line.
The third point is a consequence of the simple semicolon-insertion rules. I wished there was a way around it, as I prefer having the combining operator at the start of the continuation line, to emphasize what's happening there instead of hiding it after lots of other stuff.

But apart from that, most of this is quite sensible. Others have explained this in greater detail. The point is readability with less visual noise. Like Python, only I think python has a tad too little visual cues. Indentation alone isn't always sufficiently clear, so we get to keep our beloved braces.
Curlies mandatory
Speaking of braces, there are no brace-free forms of if and loops. Being a big fan of these, I think this is unfortunate. Coding style purists may like that. But in the end, I don't care too much. For really short statements, I can still do
if cur < min { min = cur }
Less Parens
One important technical reason for the previous point is the fact that those control statements no longer have parens. Only Perl6 tries to parse paren-less if without curlies, and we all know how complicated Perl parsers used to be (and obviously, still are). So it's actually a swap in what is mandatory and what not. Since you need braces anyhow in most cases, this is quite sensible. It's unusual to read, you have to adapt to not having those visual delimiters, but once accustomed to it, Go code feels much lighter than C code.
Explicit naming of types, functions and variables
For introducing these, the keywords type, func and var are used. This is much clearer, you get a better reading "flow". For a more technical reason, read on.
Implicit declarations, automatic typing
Variables are always statically typed, just like in C. But they can look as if they weren't. If you leave out the type, the type is instead taken from the assignment. You may even leave out the declaration altogether by using the new declare-and-initialize operator:
foo := Bar{1,2,3}
This declares a variable called foo and assigns an object of type Bar to it, using the object initializer syntax. This is absolutely the same as
var foo Bar = Bar{1,2,3}
It does not introduce dynamic typing, it does not allow changing a declared variable's type, it does not remove the need to declare variables, it does not allow you to declare variables twice. It's really the same as before, semantically, but much lighter on syntax. Feels like an ad-hoc scripting language but still gives you the benefits of static typing.
Variable declarations are backwards
In Go, the type of a variable and the return type of functions follow the name. In C, you can easily shoot yourself:
int* ptr_to_amount, amount; // a pointer and an integer
int* ptr1, ptr2; // uh-oh, this isn't what it seems to be
Go reorders things so types containing pointer and array specifiers now read a lot better. And a single declaration now only declares a single type of variable, so the above problem can't occur:
var ptr1, ptr2, ptr_to_amount *int
var amount int
This makes even more sense with the previous feature allowing you to leave out the explicit type specification.
Pointers without arithmetic
There still are pointers, but they serve as plain references to values now. All objects are value types, so assignment copies whole objects. Pointers give you the reference semantics which is default for Java. But there is no pointer arithmetic. You are forced to express array accesses as such, and this eliminates a whole slew of security-related problems. Yes, Sir, we do have bounds checking!

So pointers are no longer a central part of algorithms. They serve the single purpose of specifying reference vs. value semantics. This is simplified by the fact that pointers have no special dereferencing member accessor. foo.Bar() works with pointers and values likewise.
Garbage Collection
The previous point makes much more sense if you were able to pass around each and every pointer safely, and if you were able to take the address of each and every value, like you cannot do in C and C++.

And you can: Memory management is handled by a garbage collector. Finally! The benefit of an integrated garbage collector over a bolt-on one like boehm-gc is that you can safely pass a pointer to a local variable or even take the address of a temporary value, and it will just work! Yay!

For all those out there who are not up-to-date with garbage collection research, you may be interested in the fact that GC is not just safer to use as it completely avoids the myriad mistakes in using malloc/free. A decent garbage collector can actually be faster than manual memory management by postponing bookkeeping until there is time and completely avoiding bookkeeping by reusing unused objects. Some of the most advanced GCs combine this with better memory efficiency and cache usage because of less fragmentation. This isn't the C64 anymore.

Go has a rather simple GC right now, but a more advanced implementation is in the works. For most cases, GC is a win, but it does have a certain overhead. In the few critical cases you can use arrays of preallocated objects.
Variable-Length Arrays
This is not about arrays whose size is determined at runtime but static thereafter. This is about things you would have to use realloc() for. Arrays always have constant size, which is a little step backward from GNU-extended C. But as a replacement you get slices.

Slices look and feel like arrays, but in reality they just map to a sub-range of a plain constant-size array. Since we have a garbage collector, slices can reference an anonymous array. That way you get true dynamically-sized arrays. There are built-in functions for resizing slices and replacing the underlying array if it gets too small, so writing a vector class on top of that is trivial.
Reflection
Go supports Reflection, i.e. you can look at arbitrary types and fetch their type information, structure, methods and so on. This is in line with Java and C++ RTTI but doesn't exist in C.
Unsized Constants
Constants can be untyped, or rather, unsized. A numeric constant can be used in any context where that kind of number is valid, and it will use the precision of the data type it is assigned to. Constant expressions are calculated with full precision and only then cast to the destination type. There are no constant size suffixes like in C.
Error Handling
Go has no exceptions. Wait—are you serious? This defies all generally accepted knowledge about safe and stable programming! Isn't that a huge step backwards?

Turns out it isn't. Be honest, when did you actually use exceptions to do fine-grained error checking and handling? Most of the time, it's like this:
try {
	openSomeFile();
	doSomeWorkOnTheData();
	yadda...
	yadda...
	yadda...
	closeItAgain();
} catch (IOException foo) {
	alert("Something failed, but there's not enough time to do proper error handling");
}
This is verbose, introduces another indentation level for little benefit, and it doesn't cure the root cause, the programmer's laziness. If you want to handle errors for each call individually, verbosity gets so bad it really impairs code clarity.

So we can as well do away with the code flow complexity and return to old-school on-site error handling. Return values have been stigmatized for years, but Go has this nice modern feature of multiple return values. So forget the utter braindeadness of atoi(), we have proper out-of-band signaling. For those who care. For those who don't, they didn't even care when Java tried to enforce error handling.

Then there is panic. It is reserved for the "can't possibly continue" style of errors. Serious initialization errors, conditions that threaten the integrity of your data or your calculations, that kind of problem. Unrecoverable errors, in short. The language runtime may also create panics, like when array bounds are exceeded.

Of course, this brings us back to the problem of cleaning up used resources. For this, the defer statement exists, and it is quite a beauty, putting error handling where it belongs, right to the site of the problem:
handle, err := openSomeFile()
if err != nil { return nil, err }
defer closeSomeFile(handle)

return happilyDoWork()
defer is almost like a finally clause in Java, but it looks like a decorated function call. It makes sure that closeSomeFile is called, no matter how the function is exited. As a side effect, you can skip closing it upon success. Less code duplication, concise and visible error handling. Multiple defers are allowed, properly called in LIFO order.

For those cases where you do want to continue after a panic, there is recover. Together, panic and recover can be (ab)used to create general-purpose exceptions again. Since they obfuscate program flow, the official recommendation is that non-fatal panics should never cross package boundaries. There is no ideal solution for error handling, so you get good support for both, and you should choose the variant that is less complex for the task at hand.
Control Structures
There is only the for loop left which can behave like a foreach thanks to the range keyword. Given that while was always a special case of for, and that syntax got lighter (see above), that's fine by me. What's even better: you can put labels on your nested loops and break out of multiple levels using them. Finally!

And you still can shoot yourself in the foot with goto. Well, not really, as the more evil things are simply forbidden. But those who like to cheat a little for simplicity's sake can do so.

All this comes at only little overhead. There are a few wrinkles, which I will cover later, but as a whole it is a great improvement over C. This alone would already be sufficient for many happy nights writing procedural code without objects.

Extensions

The true strength of Go lies in that which cannot be mapped to C, C++ or any other language mentioned so far. This is what makes Go really shine:

Type-Based Objects vs. Encapsulation
There are no classes. Types and methods on types are kind of independent of each other. You can declare methods on any type, and you can declare any type to be a new type, similar to typedef in C. The difference to either C or C++ is that such a newly named type gets its own set of methods, and that primitive types (rather, those based on them) can as well carry methods:
type Handle int64

func (this Handle) String() string {
	return "This is a handle object that cannot be represented as String."
}

var global Handle
In this example, global.String() can now be called. In effect, we get a simple object system with no virtual methods. It has zero runtime overhead, as it is actually just syntactic sugar.
Duck-Typing vs. Polymorphism
Type declarations cannot be used to make two distinct types look alike. They are distinct types, and in a strictly typed language this doesn't let you create a kind of generic type that is polymorphic. One popular example in almost every language is the conversion of a value into its string representation. The Handle type declares such a method as is the convention by Go, but it doesn't show how you can act on any type that has such a String method.

C++ uses inheritance (possibly with abstract base classes) and cast operator overloading for this. Java's toString is part of its root class and thus inherited, while other calling conventions are expressed through interfaces.

Go uses interfaces exclusively. Unlike Java, however, you don't declare that a given type conforms to some interface. If it does, it automatically is usable as that interface:
type Stringer interface {
	String() string
}
That's all we need. Automatically, Handle objects are now also usable as Stringer objects. If it walks like a duck, quacks like a duck, and looks like a duck, then it is, for all practical purposes, a duck. And now the best part: This works dynamically. The interface declaration need not be imported or even known to the programmer.

Whenever a type is used as an interface, the runtime builds a table of function pointers for that interface through its run-time reflection capability. So here we do have some runtime overhead. It is optimized, however, so penalties are relatively small. Interface tables are only calculated when actually used, and only once for each type. The run-time overhead is completely avoided if the involved types can be determined at compile time. Method dispatch should be slightly faster than Apple's (already quite cool) Objective C dispatcher.
Embedding of Types vs. Inheritance
Types serve as a kind of class, but there are crucial differences, as there is no inheritance hierarchy. In the previous example, Handle doesn't inherit any methods from int64. You can get something similar to inheritance by declaring a struct type which embeds base data types within its body: (example shamelessly stolen from "Effective Go")
type ReadWriter struct {
	*bufio.Reader
	*bufio.Writer
}
This type has all methods that bufio.Reader has, and all that bufio.Writer has. Conflicts are resolved through a simple rule. This is not multiple inheritance! Both base types exist as independent data objects within the composite type, and each method from the subtype only sees its own object. That way, you get perfectly predictable behaviour without all the woes associated with classic multiple inheritance. And without all the overhead - it again is more or less syntactic sugar, making code more expressive without the runtime cost.

This also works well with interfaces. The composite type conforms to all interfaces that at least one constituent type conforms to. Of course, this can lead to nested composition, making this a kind of inheritance. The rules that resolve ambigious method references are quite simple, and non-intuitive cases are simply forbidden: If two methods are the same at the same nesting level, it is a compile-time error. Otherwise the one with the lowest nesting level wins. As a result, the composite type may freely override methods of its constituents.
Visibility Control
The primary unit of development is the package. One or more files implement one package, and you get control over what is visible from outside the package. There is no complex visibility system, you only get visible or not. This is controlled by a typographic convention: Public names start with a capital letter, private names with a lower-case one. This works for everything that is named.

This is quite a pragmatic solution. Unlike the type system, which is at least as powerful as the competition, here Go occupies a kind of middle ground: Most scripting languages don't care about visibility at all or rely on voluntary conventions, while old-school C-oids have detailed accessibility controls. Still, it seems to be a good solution for Go's object model. Since there is no classic inheritance and embedded objects stay fully encapsuled, there is no need for a protected access specification. How this works out in practice remains to be seen.
No Constructors
The object system has no special constructors. There is the notion of the zero value, i.e. a zero-initialized value of all fields of your type. You are expected to write your code in a way that the zero value is a meaningful representation of a valid "blank" object. If that isn't possible, you can provide package functions as constructors.
Goroutines and Channels
This is the most unusual feature for such a general purpose programming language. Goroutines can be seen as extremely light-weight threads. The Go runtime maps these to pth-like cooperatively multitasked pseudothreads or real operating system threads, the former with low overhead, the latter with the expected non-blocking behaviour of true threads, so we get the best of both worlds.

Channels are typed message queues which can be buffered or unbuffered. A simple deal, really. Stuff objects into them at one side, fetch them somewhere else. Most things you'd do in concurrent algorithms don't need any more.

Go goes to great lengths to make Goroutines a first-class citizen, one that can form the core of many algorithms. Segmented stacks keep per-thread minimum stack usage low, and unless you use blocking system calls, you get the performance benefits of pseudothreads, i.e. little more overhead than a simple function call.

Combined with the fact that Go has real closures, Goroutines can be used for lots of things that aren't classic concurrent algorithms. Pythons generator functions can be modeled with them, or a custom memory manager with a free object list. Read the online docs, it's amazing how versatile cheap concurrency is.

By the way, by setting an environment variable, you can tell the Go runtime how many CPU cores you want to be used so that Goroutines will be mapped to several native threads right from the start (as opposed to the default of not starting a second thread until a blocking system call is about to be called)

Regressions

No system is perfect. There are drawbacks to Go, here is a list of things I have encountered so far:

Binary size / Runtime requirements
A basic Go binary is statically linked and about 750k in size, if built without debug symbols. This is about the same size as a similar C program. I have tested this with the Tree Comparison example available on the Go Homepage, comparing it to a structurally similar C implementation I whipped up.

gccgo can compile dynamically linked executables, but libc is on every system and not usually considered a dependency, while libgo would be an extra 8MB package. For comparison: libstdc++ is less than 1MB, libc is less than 2MB. To be fair, they do a lot less than the Go standard library. Still, it's a big difference, and a dependency.

6g/8g, the original Go compiler, produces similar executables, but they don't even depend on libc, these are truly standalone. No dynamic linking of the runtime is possible, however.

This is also of concern for small systems. Sitting right next to me is an ancient 16MB Pentium-100 laptop running X and a JWM desktop quite happily and just managing to play my music collection. It even has 5MB memory left for disk cache. Would that be possible with a system written in Go?
No Equal Rights
The language is privileged in several places. For example, the special make() function does things that can't be extended from user code. This isn't as bad as it looks at first, as you can write something that behaves almost like make(), there is just no way to plug into this language construct. Same goes for some other calls and keywords that would make sense to be extensible, like range. You are more or less forced to use goroutines and channels for extending the latter one.

I'm not sure this is actually a problem. Assuming maps, slices, goroutines and channels are implemented optimally, the impact of these restrictions is nonexistant. It doesn't impair readability or clarity of code, but it feels unfair if you are used to a "mimick anything" language like Perl or Python.
No Overloading
Overloading is a source for many semantic ambiguities. That is a good reason to leave it out. But at the same time overloading, especially operator overloading, is so damn convenient and readable, so I really miss it. Go doesn't have automatic type conversions, so things would not get nearly as hairy as in C++.

As examples what overloading could be used for, imagine a BigNum library, (numeric) vectors, matrices, or limited-range guarded data types. When dealing with cross-platform data exchange, being able to change math semantics for a special data type would be a big win. You could have a 1s-complement number type if dealing with data files from ancient machines, for example, and do arithmetic that exactly mimics a target platform instead of hoping that the current platform's semantics won't differ.
Limited Duck Typing
Unfortunately, Duck Typing is incomplete. Imagine an interface like this:
type Arithmetic interface {
	Add(other int) Arithmetic
}
The function parameter and return value for Add will limit the automatic typing. An object that has a method func (this MyObj) Add(other int) MyObj does not conform to Arithmetic. There are more examples like this, and for some of them it's not easy to decide if Duck Typing should cover them or the current rules are better. You can get into a lot of non-obvious trouble, so again it's a case of "maybe it's better we keep it simple", but again I am not totally convinced.

Russ Cox, one of the core authors of Go, states:
That doesn't work because the memory layout of a MyObj is different from the memory layout of an Arithmetic. Other languages struggle mightily with this even when the memory layouts match. Go just says no.
I guess we need to declare a func (this MyObj) Add(other int) Arithmetic instead. A compromise which gains us simplicity of the compiler and of the generated machine code.
Pointers vs. Values
I am not sure if I am happy with that pointer/value business. The all-things-are-references semantics of Java are much simpler. The C++ reference vs. value syntax is also quite nice. On the positive side is that you get more control about memory-layout and -usage of structs, especially when they contain other structs, and that value-vs.-reference semantics are more explicit at function call sites, which C++ made unpredictable.

By the way, maps and slices are reference types. I was bothered about them at first, but you can make your own objects that behave similarly: structs that contain (private) pointers, which is more or less what maps and slices are. Now if only there was a way to hook into their [...] syntax...
Boolean Context
A boolean context offers lots of possibilities of simplifying code. Unfortunately, even pointers must be compared to nil, even though !pointer is totally obvious. Even more so since there is no pointer arithmetic. Also see Wishlist item 10 above. This would make a whole lot of sense and make code short and to the point.

Given that there is already the notion of a zeroed value for every type, it's trivial to extend this to boolean context.

Missing Things

Some things didn't make it into Go, and I really miss them. I hope the community will find a way to add them, in the lightweight Go style of course. I would like to see several less significant features, but some things are wishes, and one thing I really want, and that's metaprogramming.

I'm not talking about Generics or Templates. Interfaces can replace these more or less, although due to the limitations outlined above, Duck Typing is an incomplete replacement right now.

I'm talking about the real thing, the Code-that-Generates-Code variety. Imagine the possibility to implement domain-specific languages. If I do SQL, then SQL will be part of the source code no matter what. An SQL package could offer an integrated solution of writing SQL, compile-time syntax- and type-checked. It may increase compile times, but I'd rather choose static checking of all of my code over finding out at runtime that there is a syntax error in the SQL (plus paying the parse time at every invocation instead of once).

Such a facility also could improve the evolution of the language, making experimental features easily implementable and testable before considering them for inclusion into the core language. Given a well-designed implementation, of course. No one wants the C/C++ mess all over again.

Operator/method overloading would make much sense with that, so count these in. Together Go would gain a lot of expressiveness. The Python model with specially named methods has decent semantics and solves real-world problems, for example.

Unfortunately, this has been discussed lots and lots of times already, and for a newcomer like me, there is simply too much discussion and too little results to see where this is going. To quote one poster:

> Feedback is very welcome.

Search the list for each of those topics and find 1000s of e-mails for each.

This is troubling. If certain topics have been discussed so many times, why is nothing of this documented officially? Arguments like these discourage potential contributore. At the same time, regulars get tired of replying.

At best, there would be a community proposal process (like Python, Java, Perl6, XMPP, etc. have) which tracks these suggestions, outlines requirements that must be fulfilled for a given proposal to be seriously considered, and summarizes the current state of development. Then, ideas can mature, can be rejected with comprehensible arguments, can be replaced by even better ideas, and finally make it into the language without compromising its design goals. From well-documented rejections, potential contributors can learn what to avoid and what not to expect, and what to do instead.

This need not be democratic. There have been as many successes of the "Bazaar" approach as for the "Benevolent Dictator" approach to project management, and equally failures for both. More important is that there is such a process, and that it is transparent and comprehensible.

Everything said, don't get a wrong impression. The community is not of the stubborn, arrogant kind. They do listen. It's more like another poster said on the mailing list:

The problem you run into is that once you add a feature, you can never take it back.

I totally subscribe to that. I want metaprogramming, it is so incredibly convenient, but it has to be fully in-line with Go's strengths. We can leave half-assed crap for PHP.

Open Questions

These bits are unclear to me right now. As I get or find answers to these questions, I will update this review. Some of these are largely hypothetic, while other are of direct interest. Judge yourself. Text in italics is my own summary of the answers I got, they aren't quotes unless identified as such.

Feedback

There has been quite some feedback, so I added this section. It addresses interesting remarks that didn't fit into my main train of thought. Remarks that clarify my original text or that made me go "Yeah, of course, how could I miss that?" were integrated into the text above.

Ternary operators

One reader detailed the history of Pythons ternary operator a if foo else b. Given that python already is high on the readability scale, one may wonder why they added this construct which is usually frowned upon.

Seems like Python didn't have it for a while, and people lived happily with rich booleans alone: some_value or "default-value". Up until someone had the idea to do condition_to_test and value_if_true or value_if_false. From there it went downhill. That hack has drawbacks, so other hacks were invented. Each one uglier than the previous, until finally, Python got a ternary condition operator.

What does this mean for Go? Well, if you can't fight it, embrace it. Please.

D

I was not going to talk about D for a good reason. I didn't miss it, but some people didn't discover my 6-word summary of my opinion about D. Rather, I don't have experience in it. I didn't write about D because I didn't write in D. So now, by popular demand, my uninformed opinion about D:

When I first encountered D (on heise.de news, you can find select articles in english on The H), I was quite impressed. There was a language that really tried to address the deficiencies of C/C++/Java. But after skimming the docs for a while, I lost interest.

Why? Simple, because it didn't scratch the real itch. Yes, it offers everything in a single language which must be replicated in C++ with templates and which in Java has made it into yet another abstraction layer, but that's the point: Been there, done that. It may be less efficient in those languages, but they map to D pretty straightly.

While the all-features-in-a-unified-language approach is interesting, I can't see where it fits into my universe. It doesn't try to be slim. It doesn't offer any new concept worth exploring. It simply doesn't address my needs, so why should I switch and take the hassles of not having code that I can share with others, code that I can publish as open source and easily get into public use, code that interfaces easily with other people's code?

If I embark on a big project that needs all the bells and whistles, I would chose Java or C++, depending on (among others) what language interoperates best with the project requirements, especially foreign code. D simply won't appear in there. If it's a small project, D's qualities do nothing for me. The incentive to switch simply isn't there. Additionally, I dislike the "heat-seeking" approach of putting everything into the language. It's a corporate language, but corporate does Java and C#.

You know what the funny thing is? D is the reason why I rejected Go at first. I read about Go at the same place, and I thought "Oh well, there goes another one..." and ignored the news. Only when I read about it two more times, some fact (which I don't remember) sparked my curiosity, and two weeks later I am writing this review.

Go actually is different. It gives you a reason to switch. It addresses my needs and gives me something no other general-purpose language has. No, Haskell, Erlang etc. don't count. I like those from a scientific point of view, but I'd like to still understand my code when I'm tired, and I'd like to be able to make others easily understand my code. But most importantly, Go interoperates well with the outside world thanks to gccgo, and I will gain by using it, even if it should stay a relatively unknown language.

This Article on teh Internetz

Thanks to The Internets for all the discussion I didn't notice and had to be pointed to. There has been a lot of speculation about various aspects, and I found it entertaining. You didn't disappoint me. It's unlikely you will read this, but just in case, some replies to things not already covered in today's updates:

One very inportant thing many people missed is that a programming language is a tool. Once upon a time, I was deep into Perl 5, learning all the black arts over time. But do you know what was the real reason to stay? It wasn't the beauty of the language that fascinated me (there is little). Partly, it was the meta-trickery you could pull off and get away with it, that appealed to my hacker heart. But the real reason was the package archive and the ad-hoc way you could Just Write Code. These enabled me to solve real world problems in less time than otherwise.

That's why I program AVRs in C, GUI stuff in PyQt, really old GPUs in ARB_vertex_program assembler instead of GLSL, lightweight GUI stuff in C++/Fltk, Emacs in Lisp, my homebuilt embedded car MP3 player in C after prototyping it in Perl, an ARM decompiler in Python, and yes, web services meant to be maintained independently in PHP. I hate PHP, but it's the right tool for that job. So get over it and begin producing things with what works for you, check the whole picture, and keep rechecking it.

This is what I expect of Go, and currently it seems to fulfill that. I have seen lots of things that can go wrong over time, no one and nothing is safe. But I am a chronic early adopter. And the things I do, I do as hard as I can. If you want to know how it worked out, watch this space. All I can tell is that it's worthwhile to continue, something that few languages gave me.

One thing I am totally curious about are the hints at better language design, often in sentences like "... ignores 20+ years of research." Vague suggestions only work for well-known phenomena, not for specialist topics. Anyone who can point me at results relevant to Go and which fit into the constraints of the design goals, you are welcome. There is an email link at the top!

You may skip the rest of this section if only interested in programming and Go. The remaining paragraphs are off-topic or meta.

To all of you whose toes I have stepped on in my rant: It's a rant. It's not meant to be totally objective. It uses sarcasm and exaggregation. Some people got it, some didn't. Usually, the people who got it were the ones who made similar experiences as I did. And boy was it good to vent publicly. I still like C, I will still use C++ and Java when they are the correct tool for the job, but this doesn't mean they are what make my programmer heart happy ever after.

By the way, there is a difference between the phrases "sth. is not really suitable for sth." and "it is impossible to successfully do sth. with sth.", even when they appear in the vicinity of the number 10000.

Those two people who accused me of not getting OO respectively Concurrency right, care to elaborate? I can't see where there is a fundamental misunderstanding, save for some simplifications. Educated readers will know the true details, while people without the theoretic background will get the general idea of what I am referring to.

To the Perl6 fan who was offended by my sidestab at the development time that Perl6 took: Rest assured that I am still, in general, a Perl fan. I have Rakudo Star installed on my machine, and tried to write a serious utility with it in order to see whether it was any good. Just the way I did with Go. And no, Perl6 is not ready for public consumption yet, although I must admit, it's getting close.

To all nitpickers about the classification and choice of languages I rant about: Of course I can only talk about what I know. Nowhere does it say I rant about the C family or even Algol-descendant languages. In the introduction I explicitly mention several other languages that have been some system's main programming language, and that I restricted myself to the most well-known ones. Yes, JavaScript belongs there. It is one of the most widely deployed languages, it is more than relevant.

Finally, to the guy who didn't take me serious because of not getting "it's" vs. "its" right: I am terribly sorry. You are right, there was one instance in this 65kB text where I mistyped it. It is corrected, I hope you are now able to read it. Feel free to point me at other typos, since I don't use any kind of spell checking (except of you, obviously).

Conclusion

Anyone comparing the appearance of Go source code to his favourite C-oid gets a chance to express her basic attitude: Is Go partly like what you love, or partly like what you hate? It has a bit of annoyance for everyone, which is what you get when trying to do better. It creates an own feeling, so you can't claim it just mimics some other language. Well done!

In the end, we get a simple and clean type system that is easy to learn, but unusual. It might be a major obstacle in selling Go to CS beginners, who usually get taught classic OO principles. Its simplicity and safety, however, make it well-suited for self-taught programmers.

"Effective Go" is a great document to read if you come from a classic OO background. It's important to have such a document, but it is not complete enough. Researching and writing this review has taught me more about Go than writing the application which I did in parallel. There are so many things you are used to do which Go does differently but equally well (or even better), and I wasn't aware of them. There is a constant feeling of "Go can't do X" while it actually can do it well, only way differently.

For a fair image of what Go's potential is, note the age of Go. The first release was less than 2 years ago and declared stable enough since this year. Look at what Go already does today, and imagine what would be possible if Go had the same commercial backing as Java or JavaScript have. The best example of a successful introduction of a new language is Java, and now compare Go's feature set to that of Java 1.0. We have a winner here.

But to leverage that potential, the language needs some momentum. Either through an open, active and growing community, or through corporate backing. I'd prefer the community, but for real-world success, there probably has to be some corporate involvement. Bonus points if Oracle messes up the Java business even more :)

Really, Go can be the answer to the shortcomings of all currently popular system programming languages, it just needs adoption.

And as a final note, I have seen a fair amount of criticism of Go on the internet, which I cannot ignore, so here it goes: Most of these people didn't actually look at it. Go is different, even though it still looks kinda-C. It isn't. It's not C++, nor Objective C, and it doesn't try to be! So stop saying "Who needs Go when we have C++/Objective C?" already. Check out how Go tries to solve the same problems in a radically different way. Accept the fact that OO can be done in different ways. You may have opted to ignore it, but if you use JavaScript, you already use something that isn't class-based OO. Do not just accept it, actively use the power of that different approach. To the other ones, those who think Go isn't taking this far enough: Remember this is a real-world language. And it is there. And it works. What use is a beautifully constructed language that doesn't get stable, finished or fast enough for real-world problems? It's easy to nitpick on details, but to make it a real product, you need to address all constraints. That's what Go does.

And as a final final note: Big thanks to the community. Your feedback was very valuable. Please continue to correct my mistakes, misconceptions and everything, should there any remain. I hope this document will help other potential users to get into Go. But for this, it needs to be correct. Please be picky.

Document History


  2011-06-09  updated: answered another question
  2011-06-09  added: Feedback on internet discussion
  2011-06-09  added: Feedback on ternary operator
  2011-06-09  updated: answered another question
  2011-06-09  added: Control Structures
  2011-06-09  updated: Final words
  2011-06-09  updated: Embedding
  2011-06-09  added: Error Handling
  2011-06-09  updated: Variable Declarations
  2011-06-09  updated: Pointers vs. Values
  2011-06-09  updated: Limited Duck Typing
  2011-06-09  updated: answered most of the questions
  2011-06-09  updated: range keyword
  2011-06-09  updated: Parens Optional -> Less Parens
  2011-06-09  added: Opinion about C#
  2011-06-09  added: Opinion about D
  2011-06-08  removed one-file-is-one-package misconception
  2011-06-07  initial release

License

Creative   Commons License
The Go Programming Language, or: Why all C-like languages except one suck. by Jörg Walter is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.