Comments:"On Go"
URL:http://dehora.net/journal/2013/06/01/on-go/
This is one of a series of posts on languages, you can read more about that here.
Herein a grab bag of observations on the Go programming language, positive and negative. First, a large caveat - I have no production experience with Go. That said I'm impressed. It's in extremely good shape for a version 1. To my mind it bucks some orthodoxy on what a good effective language is supposed to look like; for that alone, it's interesting. It makes sensible engineering decisions and is squarely in the category of languages I consider viable for server-side systems that you have to live with over time.
You can find more detail and proper introductory material at http://golang.org/doc. And this is my first observation - the language has great documentation.
What's to like?
I’m gonna go get hammered with Papyrus
Syntax matters. In all languages I've used and with almost all teams, there's been a need to agree syntax and formatting norms. And so, somebody, invariably, prudently, fruitfully, gets tasked with writing up the, to be specific, company style guide. Go eschews this by supplying gofmt, which defines mechanically how Go code should be formatted. I've found little to no value in formatting variability with other languages - even Python which constrains layout more than most doesn't goes far enough to lock down formatting. It's tiring moving from codebase to codebase and adjusting to each project's idiom - and open source now means much of the code you're working with isn't written to your guidelines. Another benefit of gofmt is that it makes syntax transformations easier - for example the gofix tool is predicated on gofmt. This isn't as powerful as a type driven refactoring but is nonetheless useful. So while I gather it's somewhat controversial, I like the decision to put constraints on formatting.
As is the way with recent idiom in languages, semi-colons are gone, no suprise there. Interestingly, there's no while or do, just for, something I like so far. If statements don't have parens, which took me a while to get used to, but have come to make visual sense. If statements on the other hand, don't have optional bracing, which I really like. A piece of me dies every time someone removes the carefully crafted if braces I put around one-liners in Java. Go doesn't have assertions - having seen too much sloppy code around assertions, specifically handling the failure itself I think this is a good decision. The import syntax using strings (like Ruby's require statement), something I'm not crazy about; but what's interesting is that these are essentially paths, not logical packages.
Declaration order reverses the usual C/C++/Java way of saying things. This is similar to Scala and is something I like because it's easier when speaking the code out - saying 'a of type Int' is less clumsy than 'the Int named a' - the latter sounds like a recording artist wriggling out of a contract. Go has simple type derivation when using the ':=' declaration and assignment operator, albeit somewhat less powerful than again say, Scala inference.
Syntactically the language reminds me of JavaScript and Groovy, but feels somewhat different to either. The end result is a comprehensible language. Go has minimal variation and no direct support in the language for DSL-like expression. The hardest part I've found to be reasoning with pointers, but even those get manageable. What you see is more or less what you get - if you value readability over self-expression (and not everyone does, but I do), this is a big win.
You had just one job
There are no exceptions in Go and no control structures at all for error handling. I find this a good decision. I've believed for a long time that checked exceptions are flawed idea. Thankfully this isn't controversial anymore and all's right with the world. Runtime exceptions are not much better. Maybe try/finally is ok, but as we'll see, Go has a more concise way to express code that should execute regardless.
So you handle errors yourself. Go has an inbuilt interface called error -
type error interface {
Error() string
}
and allows a function to return it along with the intended return type, which has a surface similarity with multiple return values in Groovy/Python. Now, I can see this kind of in place error handling driving people insane -
if f, err := os.Open(filename); err != nil {
return err
}
but I prefer it to try/catch/finally, believing that a) failures and errors are commonplace, b) what to do about them is often contextual such that distributing occurence and action isn't the right default. Pending some improved alternate approach or a real re-think on exceptions, it's better not to have the feature.
Because there are no exceptions there's no finally construct - consequently you're subject to bugs around resource handling and cleanup. Instead there is the 'defer' keyword, that ensures an expression is called after the function exits, providing an option to release resources with some clear evaluation rules.
Go has both functions and methods and they can be assigned to variables to provide closures. Functions are just like top level def methods in Python or Scala and can used directly without an enclosing class structure via an import. Methods are defined in terms of a method receiver. A receiver is instance of a named type or a struct (among others). For example the sum function below can be used against integers whenever it is imported -
package main
import "fmt"
func sum(i int, j int) (int, error) {
return i + j, nil
}
func main() {
s, err := sum(5,10)
if err != nil {
fmt.Println("fail: " + err.Error())
}
fmt.Println(s)
}
Alternatively, to make a method called 'plus' the receiving type, is stated directly after 'func' keyword -
package main
import "fmt"
type EmojiInt int
func (i EmojiInt) plus(j EmojiInt) (EmojiInt, error) {
return i + j, nil
}
func main() {
var k EmojiInt = 5
s, err := k.plus(10)
if err != nil {
fmt.Println("fail: " + err.Error())
}
fmt.Println(s)
}
This composition drive approach leads us into one the most interesting areas of the language, that's worth its own paragraph.
Go does not have inheritance or classes.
If it helps at all, here's a surprised cat picture -
While OO isn't clearly defined, languages like Java and C# are to a large extent predicated on classes and inheritence. Go simply disposes of them. I'd need more time working with the language to be sure, but right now, this looks like a great decision. Controversial, but great. You can still define an interface -
type ActorWithoutACause interface {
receive(msg Message) error
}
and via a structural typing construct, anything that implements the signatures is considered to implement the interface. The primary value of this isn't so much pleasing the ducktyping crowd (Python/Ruby developers should be reasonably ok with this) and support of composition, but avoiding the premature object hierarchies typical in C++ and Java. In my experience changing an object hierarchy is heavyweight, and it requires effort to avoid creating one early. These days I'm reluctant to even define Abstract/Base (implementation inheritance) types - I'll use a DI library to pass in a something that provides the behaviour. I'd go as far as saying I'd prefer duplicated code in early phase development to establishing a hierarchy (like I said it requires effort). Go lets me dodge this problem by providing functions that can be imported but no way to build up a class hierarachy.
This ain't chemistry, this is art.
Dependency seems to be a strong focus of the language. Depedendecies are compiled in, no dynamic linking. Go does not allow cyclic depedencies. Consequently build times in Go are fast. You can't compile against an unused dependencie - eg this won't compile -
package main
import "fmt"
import "time"
func main() {
fmt.Println("imma let u finish.")
}
prog.go:3: imported and not used: "time
which may seems pedantic but scales up well when reading existing code - all imports have purpose. I mentioned that Go builds are fast. Actually they're lightning fast, fast enough to be used for short scripts. You can use the dependency model to fetch remote repositories, eg via git. I have more to say on that when it comes to externalities.
Package visibility is performed via capitalization of the function name. Thus Foo is public, foo is private. I'll take it over private/public/protected boilerplate. I would have gone for Python's _foo idiom myself, but that's ok, it's obvious what's what when reading code.
Go doesn't have an implicit 'self/this' concept, which is great for avoiding scoping headaches a la Python, as well as silly interview questions. When names are imported, they are prefixed -
package main
import "fmt"
import "time"
func main() {
time.Sleep(30)
fmt.Println("imma let u finish.")
}
such that imported names are qualified, all unbound names are in the package scope. Note how I still have to qualify Sleep and Println with their time and fmt packages. I love this - it's one of my favorite hygenic properties in the language. If you dislike static imports in Java as much as I do and the consequent clicking through in the IDE to see where the hell a name came from, you may also like what Go does here.
Go allows pointers. For the variable 'v', '&v' holds the address of v rather than its value.
package main
import "fmt"
func main() {
v := 1;
vptr := &v
fmt.Println(v)
fmt.Println(vptr)
fmt.Println(*vptr)
fmt.Println(*&v)
}
1
0xc010000000
1
1
Overall this enables by reference and by value approaches; for some usecases it's useful to avoid a data copy (technically, passing a pointer creates a copy of the pointer, so ultimately its all pass by value). Thankfully there's no pointer math, so JVM types like myself don't need to freak out on seeing the '*' and '&' symbols (and in case you're wondering arrays are bounds checked). Go has two construction keywords for types - new and make. The new keyword allocates memory but doesn't initialise and returns a pointer. The make keyword performs allocation and initialization and returns the type directly.
There aren't any numeric coercions, so you can't do dodgy math over different scalar types. This isn't really a feature because languages that allow easy coercions like this are arguably broken. Still, given Go's design roots in C and C++, I'm happy to see that particular brokeness wasn't brought forward. It still won't stop anyone using int32 to describe money however.
You have just saved yourself from a fate worse than the frying pan
The Go concurrency model prefers CSP to shared memory. There are synchronization controls available in the sync and atomic packages (eg CAS, Mutex) but they don't seem to be the focus of the language. In terms of mechanism, Go uses channels and goroutines, rather than Erlang-style actors. With actor models the process receiver gets a name (in Erlang for example, via a pid/bif), whereas with channels the channel is instead the thing named. Channels are are sort of typed queue, buffered or unbuffered, assigned to a variable. You can create a channel and use goroutines to produce/consume over it. To get a sense of how that looks, here's a noddy example -
package main
import "fmt"
import "time"
func emit(c chan int) {
for i := 0; i<100; i++ {
c <- i
fmt.Println("emit: ", i)
}
}
func rcv(c chan int) {
for {
s := <-c
fmt.Println("rcvd: ", s)
}
}
func main() {
c := make(chan int, 100)
go emit(c)
go rcv(c)
time.Sleep(30)
fmt.Println("imma out")
}
Channels are pretty cool. They provide a foundation for building things like server dispatch code and event loops. I could even imagine building a UI with them.
As you can see above it's easy enough to use goroutines - call a function with 'go' and you're done. Once they're created goroutines communicate via channels, which is how the CSP style is achieved. That said you can use shared state such as mutexes but the flavour of the language is to work via channels. Goroutines are 'lightweight' in the Erlang sense of lightweight rather than Java's native threads. Being 'kinda green' they are multiplexed over OS threads. Go doesn't parallelize over multiple cores by default as far as I can tell; like Erlang it has to be configured to do so. It is possible to allot more native threads to leverage the cores via the runtime.gomaxprocs global, which says 'this call will go away when the scheduler improves'; it will interesting to see what happens in future releases. Go's default is closer to Node.js with the caveat it can be made multicore whereas Node can only run single threaded. Otherwise, the approach to scaling out seems to be to use rpc to dispatch across multiple go processes for now. As best as I can tell a blocking goroutine won't block others as the other goroutines can be shunted onto another native thread, and it seems that blocking syscalls are performed by spawning an additional thread so the same number of threads are left to run goroutines, but my knowledge of the implementation is insubstantial, so I might have the details wrong.
Externalities
Onto some things that bother me about Go.
Channels are not going to be as powerful as Actors used in conjunction with pattern matching and/or a stronger type system. Mixing channel access with switch blocks seems possible if you want to emulate an actor style receive model, but it'll be lacking in comparison Erlang and Scala/Akka that underpin their various actor models. That said, channels seem more than competitive in terms of the all import concurrency/sanity tradeoff when compared to thread based synchronization. I can't imagine wanting to drop into threads after using channels.
The type system in Go is antiquated. If you're bought into modern, statically typed languages such as Haskell, Scala OCaml and Rust and value what they give you in terms of program correctness, expresiveness, and boilerplate elimination, Go is going to seem like a step backwards. It is probably not for you. I'm sympathetic to this viewpoint, especially where efforts are made to match static typing with coherent runtime behaviour and diagnostics, not just correctness qualities. On the other hand if you live in the world of oncall, distributed partial failures, traces, gc pauses, machine detail, and large codebases that come with their very own Game of Code social dynamics, modern static typing and its correctness assurances don't help much. Perhaps the worst aspect with Go is that you are still subject to null pointers via nil; trapping errors helps but not as much as a system that did its best to design null out, such as Rust.
Non-declaration of interface implementation feels in a kind of chewy yet insubstantial way, like a feature that isn't going to scale up. I imagine this will be worked around with IDE tooling providing implements up/down arrows or hierachy browsers. Easier composability via structural typing is possibly a counter-argument to this if the types in the system interact with less complexity than sub-type heavy codebases, something that's achievable to a point with Python/Scala but impractical in Java. So I'm ready to be wrong about this one.
Go concurrency isn't safe, for example, you can deadlock. Go is garbage collected, using a mark/sweep collector. While it's probably impossible for most of us to program concurrent software and hand manage memory, my experience with Java is a lot of time dealing with GC, especially trying to manage latency at higher percentiles and overflowing the young generation. Go structs might allow better memory allocations, but I don't have the flight time to say GC hell will or won't exist in Go. It would be very interesting to see how Go holds up under workloads witnessed by datastores such as Hadoop/HBase, Cassandra, Riak and Redis, or modern middlewares like Zookeeper, Storm, Kafka and Rabbitmq.
The 'go get' importing mechanism is broken, at least until you can specify a revision number. I'd hazard a guess and say this comes from Google's large open codebase, but I've no idea what the thinking is. Having worked in a codebase like that I can see how it makes sense along with a stable master policy. But I can also see stability will suffer from an effect similar to SLA inversion, in that the probability of instability is the product of your externally sourced dependencies being unstable. It's important to think hard about your dependencies, but in practice if you have make an emergency patch and you can't because you can't build because of upstream changes you are SOL. A blameless post-mortem that identified inability to build leading to a sustained outage is going to result in a look of disapproval, at best. I don't see how to protect from this except by copying all dependencies into the local tree and sticking with path based imports, or using a library based workaround. The former complicates bug propagation in the other direction resulting in rot. Put another way using sneaky pincer reasoning - if Go fundamentally believed this was sane, the language and the standard libraries wouldn't be versioned. Thankfully it's a mechanism rather than a core language design element and should be something that can get fixed in the future.
Conclusions
Go seems to hit a sweetspot between C/C++, Python JavaScript, and Java, possibly reflecting its Google heritage, where those languages are I gather, sanctioned. It seems to be trying to be a more effective language rather then a better language, especially for in-production use.
Should you learn it and use it? Yes, with two caveats. How much you like static type systems, and how much you value surrounding tooling.
If you really value modern, powerful type systems as seen in Haskell, Scala and Rust, I worry you'll find Go pointless. It offers practically nothing in that area and is arguably backwards looking. Yes there is structural typing, and (thankfully) closures, but no sum types, no generics/existentials, no monads, no higher kinds, etc - I don't think anyone's going to be doing lenses and HLists in Go while staying sane.
An issue is whether significant investment into the Go runtime and diagnostic tooling will happen outside Google. Tools like MAT, Yourkit, Valgrind, gcviz etc are indescribably useful when it comes to running server workloads. The ecosystem on the JVM for example, in the form of the runtimes, libraries, diagnostic tools, and frameworks, is like gravity - if anything was going to kill Java, it was the rise of Rails/Python/PHP in the last decade - that didn't happen. I know plenty of shops are staying on the JVM, or have even moved back to Java, mostly because of its engineering ecosystem. This regardless of the fact the language has ossified. JVMs have been worked on for nearly two decades, by comparison Go's garbage collector is immature, and so on.
A final thought. Much code today is written in a language that can be considered to have a similar surface to Go - C, C++, C#, Python JavaScript, and Java. If you buy into the hypothesis that programming language adoption at the industry level is slow and highly incremental, then Go's design center is easy to justify and it very broad adoption possible, given either a killer application (a la Ruby on Rails for Ruby and browsers for JavaScript) or a set of strong corporate backers who bet on the language (a la Java and C#). Aside from generics and possibly annotations, there's a reasonable argument to be made that Go is sufficiently more advanced than Java and JavaScript without being conceptually alien, and good enough compared to C#, Python and C++. Plenty of shops don't make decisions based on market adoption, but for larger engineering groups it's inevitably a concern.
2013/06/02: updated the Node/Erlang concurrency and clarified pointer observations with feedback from @jessemcnelis. Somehow in this wall of text, I forget to mention readability, thankfully was reminded by @davecheney