Lab notes: tinkering with Go
Recently I designed a utility for my research. To implement this utility I needed a programming language that: 1) is appropriate for fast prototyping, 2) supports asynchronous, low-latency event handling, and 3) supports efficient in-program multi-way channel polling.
So far I could find, only Go fits the bill, and so I learned Go for a couple of days.
Report inside.
This is really a case of choosing the right tool for the job.
The specific feature I needed, “in-program multi-way channel polling” means the ability to wait on two or more communication channels until any of them is ready for reading or writing.
This feature is present in POSIX via the select(2) system call (or poll(2), epoll(2), etc on other Unix variants) but these system calls are designed for inter-process communication: they use file descriptors and incur the cost of a system call. Instead, I needed a lightweight channel implementation between lightweight tasks within the same process, and the equivalent of select(2) for these channels.
My usual go-to language of choice for fast prototyping, Python, does not provide this feature. My other go-to languages, namely O’Caml, the Unix shell, and recently Haskell, do not provide this feature natively either. Other languages I looked at provide a select-like API to test the availability of data on two or more channels for reading only, but not mixed readability/writeability tests. From the theory standpoint, what I was really looking for is a CSP-derived language; as of this day, Erlang and Go are currently the only production-ready languages that are also directly derived from CSP principles. Of these two, only Go compiles to native code and provide good native support for text strings, which I also needed.
So off I went with Go, and I temporarily set aside all my resistance against anything I would otherwise judge as poor language design decisions in order to “get the job done.”
Approximately two weeks and 1000 lines of code later, I am happy to report that my utility works and is as insanely flexible as I intended it to be. From a productivity standpoint, I clearly feel Go saved me a great amount of time, compared to what it would have cost me to implement the same with any of the other languages named above. So, there’s that.
The three main features I enjoyed most and would attract me again towards Go are:
- its select statement, which was the reason I chose Go in the first place;
- its no-surprise execution model: code runs sequentially an uninterrupted until it blocks or terminates; “goroutines” may run in separate OS threads but cannot be constrained to. An external parameter sets the number of OS threads to use. A special exception is made for goroutines waiting on an OS syscall: these are always forced to run in separate OS threads, so that a blocking OS syscall does not prevent runnable goroutines from progressing.
- gofmt: reformats the source code to follow the One Style Standard of Go. I don’t have to care about configuring my editor or keeping my punctuation in the right places; gofmt fixes anything wrong for me when I save my files, eg. using (add-hook 'before-save-hook #'gofmt-before-save) in Emacs. Added bonus: no need to adjust when reading code from other people either, since everyone uses it.
Go is also designed as a Unix language, meaning it also provides comprehensive support for syscalls and interfacing with C programs / libraries. This is good for implementing system glue code, which I am likely to do soon.
The things that could have grated me immensely but where I eventually agreed with myself to compromise:
- the declaration syntax, with types coming after the name of the variables. That required some adjustment and I still make the occasional mistake, but I eventually accepted it as a minor cultural difference.
- the silly white space rules (eg. one cannot place the opening brace of an if statement on the following line, it’s a syntax error). This is still hard to let go, but the goodness of gofmt makes it worth the effort.
- using the case of global identifiers to determine external visibility (only identifiers starting with a capital letter are exported in a module). I still hate it fiercely; I don’t like capitals in my code and I still prefer Python’s standard where underscore prefixes make identifiers private. However, this restriction was not hurting too much for my relatively simple goal, so I just compromised this one time.
Finally, the things that really hurt and that would push me away from Go for larger or more complex tasks:
- the lack of both macros and type-generic functions. Dear Go designers, seriously?
- tagged union types with pattern matching checked statically. Currently Go has a generic type called “interface{}” with a dynamic pattern matching (type switches), but the compiler can’t detect incomplete matches.
- operator overloading. Or the ability to define new operators. This prevents me from extending the language and embedding my own DSL s in Go for research purposes.
My other go-to languages, namely C, Python, ML, Haskell, LISP, Chapel and C++ all provide some variants of these features. They are not detrimental to performance and are known to improve language expressiveness and programmer productivity. So what’s wrong here?