The Go programming language is was on my (long) list of things to look into for quite some time now. Recently, at last, I had the opportunity to go through the most part of a comprehensive tour of Go from the official website, as well as write few bits of some Go code by myself.
Today I’d like to recap on some of my impressions. You can treat it as “unboxing” of the Go language, much like when people post movies of their first hands-on experiences with new devices. Except, it will be just text – I’m not cool enough to do videos yet ;)
We all like to put stuff into our various mental buckets, so let’s do that with Go too.
Go is a compiled, statically typed programming language that runs directly on the hardware, without any underlying virtual machine or other bytecode-based runtime. That sounds good from the speed viewpoint and indeed, Go comes close to C in raw performance of equivalent programs.
Syntax of Go is C-like, at least in the fact that it’s using curly braces to delimit blocks of code. Some visual clutter is intentionally omitted, though. Semicolons are optional, for example, and idiomatic Go code omits them at all times.
But more surprisingly, parentheses around if
and for
conditions are straight out forbidden. As a result, it’s mandatory to use curly braces even for blocks that span just one line:
If you’re familiar with reasoning that suggests doing that in other C-like languages, you shouldn’t have much problems adapting to this requirement.
Go is type-safe and requires all variables to be declared prior to use. For that it provides very nice sugar in the form of :=
operator, coupled with automatic type inference:
But of course, function arguments and return values have to be explicitly typed. Coming from C/C++/Java/etc. background, those type declarations might look weird at first, for they place the type after the name:
As you can see, this also results in putting return type at the end of function declarations – something that e.g. C++ also started to permit.
But shorthand variable declarations are not the only way Go improves upon traditional idioms of static typing. Its interfaces are one of the better known features here. They essentially offer the support for duck typing (known from Python, among others) in a compiled language.
The trick is that objects do not specify which interfaces they implement: it’s just apparent by their methods. We can, however, state what interfaces we require for our parameters and variables, and those constraints will be enforced by the compiler. Essentially, this allows for accepting arbitrary values, as long as they “quack like a duck”, while retaining the overall type safety.
As an example, we can have a function that accepts a very general io.Writer
:
and use it with anything that looks like something you could write into: file objects, networked streams, gzipped HTTP responses, and so on. Those objects won’t have to declare or even know about io.Writer
; it’s sufficient that they implement a proper Write
method.
Talking about objects and interfaces sounds a bit abstract, but we shall not forget that Go is not a very high level language. You still have pointers here like in C, with the distinction between passing an object by address and copying it by value like in C++. Those two things are greatly simplified and made less error prone, however.
First, you don’t need to remember all the time whether you interact with object directly or through a pointer. There’s no ->
(“arrow”) operator in Go, so you just use dot (.
) for either. This makes it much easier to change the type of variable (add or remove *
) if there’s need.
Second, most common uses for pointers from C (especially pointer arithmetic) are handled by dedicated language mechanism. Strings, for example, are distinct type with syntactic support and not just arrays of char
s, neither a standard library class like in C++. Arrays (called slices) are also well supported, including automatic reallocation based on capacity, with the option of reserving the exact amount of memory beforehand.
Finally, the common problems with pointer aliasing don’t really exist in Go. Constraints on pointer arithmetic (i.e. prohibiting it outright) mean that compiler is able to track how each and every object may be used throughout the program. As a side effect, it can also prevent some segmentation faults, caused by things like local pointers going out of scope:
The i
variable here (or more likely: the whole stack frame) will have been preserved on heap when function ends, so the pointer does not become immediately invalid.
If you ever coded a bit in some of the newer languages, then coming to C or C++ you will definitely notice (and complain about) one thing: lack of proper package management. This is an indirect result of the header/implementation division and the reliance on #include
‘ing header files as means of specifying dependencies. Actually, #include
s are not even that: they work only for compiler and not linker, and are in some sense abused when working with precompiled headers.
What about Go?… Turns out it does the right thing. There are no separate header and implementation units, only modules (.go files). Unless you are using GCC frontend or interfacing with C code, the compiler itself is also unified.
But most importantly, there are packages and normal import
statements. You can have qualified and unqualified imports, and you can alias things you’re importing into different names. Packages themselves are based on directory structure rooted in $GOROOT
, much like e.g. Python ones are stored under $PYTHONPATH
.
The only thing you can want at this point is the equivalent of virtualenv. Note that it’s not as critical as in interpreted languages: standalone compiled binaries do not have dependency problems, after all. But it’s still a nice thing to have for development. So far, people seem to be using their own solutions here.
Lacking a small thing like that is not nearly a deal-breaker. Ironically, there’s something else that Go scores minus points for providing: automatic memory management.
Or in plain English: Go has garbage collection. Boo-hoo!
On more serious note, you may wonder how this can be a bad thing at all. Hasn’t every language got GC nowadays? Even Objective-C has something equivalent to reference counting garbage collector. And Go has a proper one that uses parallel mark-and-sweep, somewhat similar to Java on Android. What’s the alleged problem?
Thing is, Go intends to compete within a lower-level niche than most of those “contemporary languages”. I keep comparing it to C and C++ because they are its immediate neighbors in the whole concept-space of programming languages. Yet by having a built-in, mandatory garbage collector it sets itself somewhat apart from them.
The practical consequence is that applications of Go end somewhere in the gray area of soft real-time systems. Interactive programs in general – and games in particular – might or might not be among them. It all depends on particular case, actual efficiency of Go’s GC, hardware we want to target, etc. Even if the performance problems are often exaggerated, trade-offs are acceptable and the whole issue is just overblown, it still requires careful analysis when doing anything serious.
In contrast, C++ or (especially) C is just a safe bet.
While sporting a garbage collector distinguishes Go from C quite substantially, there’s one thing that brings both very close in terms of design decisions.
Go doesn’t have exceptions. By that I mean a typical pattern of try/catch/finally
, try/except/finally
, begin/rescue/end
, handle (\e -> ...) $
or equivalent construct. There is a little mechanism of panicking and recovering that looks superficially similar, but it’s not a normal, idiomatic way of handling most errors.
So what is? Well, return values. Strange as it may be, this actually works pretty well because it was a deliberate design decision. To make it usable, the language actually offers several related features and quality-of-life syntactic constructs.
Firstly, it is possible to return more than one value from a function. On the surface, it’s similar to tuples in Python et al., but in Go you cannot carry multiple values around – you need to unpack them right away:
The second thing is a special syntax for if
statement which allows to perform error checking on a spot, reminiscent of the infamous comma operator from C:
Third, the error
objects are not mere integer codes. Much like exceptions, they can be arbitrary objects that implement particular interface. All errors can describe themselves, for example, providing a text message ready for supplying to log
package functions.
Lastly, resource management (for non-memory resources) is solved by introduction of defer
statement. By deferring an operation, you tell the runtime to execute it at the end of current function (also when panicking) in a last-in-first-out manner.
In C, these problems are typically solved through goto
in one of its rare legitimate uses. defer
is of course much cleaner, even if it breaks the flow of reading the code line by line.
With those tools in hand, it’s not really cumbersome to handle errors in Go. You can also ignore them pretty easily: just substitute err
with _
(underscore) and omit the check.
What you cannot do easily this way is to propagate errors further up the call stack. For that you would need to modify every function along the way, in a manner more complicated than, say, adding throws
declaration in Java.
I will stop at this point, as I think I’ve covered the all the most important and characteristic aspects of Go… almost. There is still a topic of concurrency and the so-called goroutines but this one actually deserves a post on its own.
If you want more information, Go homepage would be the best place to, ahem, go. You can even try it right away in the browser.
Adding comments is disabled.
Comments are disabled.