I’ve been experimenting with the Go Programming Language for the past few days. (Thanks to Jürgen, who held two introduction sessions at work.) I have not dived very deeply into the language, but already I feel it suits me pretty well.
Go is a really clean and simple, yet powerful language. Here’s what I like so far:
Go is statically typed – I believe this catches a lot of the obvious coding errors you make in your day-to-day scripting language.
Variables are declared with :=
. As a mathematician, I like this
very much, and it’ll give you errors if you re-declare the same
variable twice.
C without the cruft and overhead – There are real string
types.
You can concatenate them on the fly. Most objects can be
stringified. For example, printing a struct with
fmt.Printf("s = %#v", s)
will print the struct in key: value
format, in turn stringifying the elements. This makes for easy
debugging.
Slices (somewhat like arrays, but really much more useful) can be
grown dynamically: sl = append(sl, args...)
– no more checking and
perhaps re-allocating space for new objects. No more for-loops over
every element: Simply range
over it.
You are allowed, even encouraged, to return to the caller variables
that were initialized on the stack. No more return xstrdup(errmsg);
.
Oh, and of course: Garbage Collection.
Clean syntax and a strict compiler – Often I have to wade through really bad C code, cursing about mixed tabs, spaces, indentations. Compilation is only possible with tons of warnings.
Not so with Go. The language is a lot like C, but the compiler is strict: You include an unnecessary package? Compilation error. You have an unused variable? Compilation error. You make a computation without assigning (storing) the return value? Compilation error.
What about tabs vs. spaces? Spaces around + signs? Before
parentheses? Alignment of struct fields? – There’s a definite
answer, and
it's called gofmt
.
To re-format all Go files, simply use gofmt -w .
. I have installed
a simple Git hook to alert me whenever I’m about to commit code
not in accordance with the style guide:
#!/bin/sh
validgo() {
d="$(git show :"$1" | gofmt -d)"
[ -z "$d" ] && return 0
echo "$d"
echo
echo "File $1 contains improper Go syntax;"
echo "please fix with 'gofmt -w $1' before committing!"
return 1
}
git diff --cached --check || exit 1
git diff-index --name-only --diff-filter=ACM HEAD | while read f; do
if [ "${f##*.}" = "go" ]; then
validgo "$f" || exit 1 # exit in subshell!
fi
done || exit 1 # make the hook fail
Easy-to-use concurrency handling – If you’ve ever tried to write a simple multithreaded C application with multiple workers and a master aggregating the workers’ results, you’ll find that’s really painful. In Go, it feels really natural.
You start off goroutines (‘lightweight threads’) with the go
keyword, which is almost like the binary &
pattern in your
average shell: The goroutine is dispatched, and the caller proceeds
without waiting for it to return.
These routines (or any other parts of your program, for that matter) should communicate solely using channels. (Read more about them here.) They are a lot like UNIX sockets: You can put stuff in them, and it comes out at the other end; they have a certain buffer size: writing blocks if that’s full, reading if it’s empty; you can close them.
You don’t have to use channels to send data; you can also use them
to synchronize events. For exaple, if you dispatch a goroutine,
you’re unable to tell when it has finished. But if you want to wait
for it to finish, you could use a channel with a buffer size of 0
(i.e. reading from it will block until someone is writing to it).
Call this channel done
and pass it to the goroutine (for example
as a paramater). From the caller’s perspective, you just wait for
something in that channel: <-done
. Once the goroutine is finished,
it’ll write some arbitrary data to that channel. If the channel is
of type chan bool
, then done <- true
will make the caller
unblock, receive the value und continue with code execution.
One thing I didn’t get at first was the close
/range
idiom: If
there’s a finite amount of data that you want to sequentially read
and handle, stopping when there’s no more data left, you can use
this idiom: A function that returns a channel where a goroutine will
write results, eventually closing the channel and thus signalling the
range
operator that this was the last result.
func compute(...) (chan T) {
c := make(chan T)
go func() {
defer close(c)
...
}()
return c
}
c := compute(...)
for res := range c {
// do something with res
}
Nice and intuitive code flow – Apart from spawning goroutines
easily, there are some really simple things that make life easier.
Namely, defer
statements and multiple return values.
You can declare functions to be called when the function returns
(like closing channel c in the above example). This eliminates the
usual C pattern where you have a label finish
with lots of
if(fd != -1) close(fd);
cases. In Go, you rather write:
f, err := os.Open(fn)
defer f.Close()
No matter where you actually throw in your return
, you are
guaranteed to have the file closed properly after the function has
returned.
Also, you can return multiple values. But you don’t have to
explicitly list them to return them. If you name them during
function declaration: func fn(...) (a, b, c int) { ... }
– then
just say return
to return the current values of a, b und c.
I have not really fully grasped the importance of interfaces, but I guess I’ll come to that in a few days.
So far, there’s one thing I don’t like: The error checking idiom
result, err := function(...)
if err != nil {
// handle error
}
It’s not the multi-value return… I find that much better than the
usual try-catch-blocks. (Quote Rob Pike: “errors are not
exceptional!”) It’s just that I’d like to check for an error, not if
the error is not nothing. From a logician’s point of view it’s the
same. But I believe if you could somehow write if err { ... }
the
code would be so much more readable. Why can’t nil
be cast to the
Bool type false
?
–
Go is really easy to start with. It took me all of one hour to do a simple client-server-application than can pass a Go struct using net/rpc.
But I am not really sure yet how well Go scales. That is, how much
parallelisation is actually good. I did a little coding exercise: On
my system, grep
is CPU-bound when the relevant files are in the disk
cache. So I thought, maybe I can simply create a multithreaded grep in
Go.
I have a simple version (simple as
in: it emulates fgrep -IR
) that uses one goroutine for every file.
The workers themselves are sent over a channel (a “channel of
channels”) so that the order of output files resembles the order of
files checked.
However, my grep is an order of magnitude slower than the real grep. I tried using the profiler, but I haven’t gotten any meaningful results out of it. If you have a clue to that problem, please write me an e-mail!