Table of Contents
Here's why I love Go
Many years ago, I've been waiting for a language like Go. Back then, I was primarily working with C (for microcontrollers), and C++/Java/Python/etc (for bigger machines). These are great tools, but still I was longing for compiled, statically typed, memory-safe (and garbage collected) language, which compiles for the target platform machine code, and has a big enough community.
I looked at the Zimbu programming language; the goals of which match mine quite well:
It has to be as fast as possible, so interpreted languages are out.
You don't want to micro manage memory, so C is out.
You don't want to require programmers to have a degree, so C++ is out.
You want fast startup and not depend on a big runtime, so Java is out.
It has to run on most systems, anything with a C compiler, so D is out.
But Zimbu is a one-man show, so obviously it didn't match the “big enough community” requirement. And actually I find some parts of Zimbu syntax totally awful:
FUNC Main() int IO.print("Hello, World!") RETURN 0 }
For example, the lack of opening brackets distracts a lot. Bram Moolenaar (author of Zimbu) is known for another language with awful syntax however: Vimscript, so nothing to be surprised about. ;)
And a few years ago, I discovered Go.
Focused on practical solutions
Go is very practical. Its design is focused on practical solutions rather than being driven by theoretical perfectionism. I often hear people complaining about it not having a feature X (indeed, Go lacks lots of features often present in other languages), but actually I do like this minimalism.
For example, consider error handling. Go doesn't have exceptions, so the common idiom is for the function to return multiple values, the last one being an error
. And, of course, the caller should check that error, and react appropriately. This is a common idiom:
value, err := DoSomething() if err != nil { return errors.Trace(err) }
On the cost of some repetition here and there, it makes programs look very explicit, which helps readers to understand the code better, instead of wondering: can the exception be thrown here or not?
(However, it's sad that very few people use this errors.Trace(err)
thing, more about it here)
Concurrency
In my opinion, the way concurrency is designed and implemented in Go is outstanding. Actually, Go supports two styles of concurrent programming: the traditional shared memory multithreading, and the encouraged communicating sequential processes (CSP).
With shared memory multithreading, we use mutexes, conditional variables and other concurrency primitives often found in other languages. With CSP, however, we have goroutines and channels.
You can think of goroutines as lightweight threads. Semantically they are threads, but extremely cheap ones. It's not uncommon to have thousands of goroutines running concurrently. Channels are means by which goroutines communicate with each other.
First of all, let me quote a paragraph from the “Share Memory By Communicating” article:
Go's concurrency primitives - goroutines and channels - provide an elegant and distinct means of structuring concurrent software. (These concepts have an interesting history that begins with C. A. R. Hoare's Communicating Sequential Processes.) Instead of explicitly using locks to mediate access to shared data, Go encourages the use of channels to pass references to data between goroutines. This approach ensures that only one goroutine has access to the data at a given time. The concept is summarized in the document Effective Go (a must-read for any Go programmer):
Do not communicate by sharing memory; instead, share memory by communicating.
For me personally, the concept of goroutines which communicate with each other by means of channels, translates very well to the concept I'm using in embedded real-time kernels (e.g. my beloved TNeo), where threads communicate with each other by means of message queues. So they were quite familiar to me from the beginning, but the fact that goroutines are very cheap opens additional possibilites, of course.
I do use mutexes and conditional variables occasionally, but still prefer goroutines and channels, as it leads to better designed programs in Go.
This topic is large enough, so I'm not going to repeat all of it here; refer to the linked articles for details.
Go is a better* C
*Better for most applications. I was hoping it's obvious, and I myself keep using C for embedded stuff I work on, but some people on reddit keep arguing that Go might also be “a worse C”, so I had to add this disclaimer.
I'm not the first person who draws a parallel between Go and C, still it's a good point to emphasize.
I wrote a lot of code in C, and I try hard to make that code good. Perhaps surprisingly, overall I do like C, except for a few things which I don't like:
- Having to micro manage memory all the time;
- Handling errors properly is a pain;
- Writing concurrent code is a pain;
- Writing cross-platform code is a pain;
- Standard library is in many aspects awful. For example, in 2017,
asprintf()
andtimegm()
are still not standardized; I just can't understand it.
Go solves all these problems for me. So, in the end, we have C without its flaws, and it sounds like a really awesome language! Yes, I think it is awesome indeed.
Great tools out of the box
Compilation is fast, crosscompilation is painless
First of all, of course, the compiler. To build a go project, there is a command go build
, and it works really fast.
Cross-compilation is surprisingly painless. I can produce a Windows binary from my Linux machine with just one command, like:
$ GOOS=windows GOARCH=386 go build
Open source
The fact that the language and its standard library are open source makes it much easier and fun to learn. Not only we can use this to figure how something works; we can learn how experts write really good Go.
Race detection
Go runtime can detect race conditions; this feature provides tremendous help given how difficult it could be at times to find such a bug in a complex system. To build the project with that feature enabled, build it with the --race
flag.
Aux
Other than that, we have tools for auto-importing packages, renaming, code formatting, etc.
The code formatting part might sound minor, but still, did you ever have to waste time arguing about the style? I did, and I don't think it's a good kind of activity. The gofmt tool doesn't even have any options to adjust the output! It just formats your code, it uses tabs for indentation and spaces for alignment, the output is certainly good enough, so we can proceed to more important things rather than arguing about the style.
Easy IDE-free development
Another point which absolutely like is that all standard tools only have command-line interface, so it's easy to integrate into any kind of workflow, instead of having to install and adapt to a new gigantic IDE, etc (that's a sensitive point for me, which I explained in a separate article). I personally just use the vim-go plugin for Vim, but you can choose whatever editor you want.
Go makes me love web development
What I totally dislike about web development nowadays is that using dynamic languages for that (like Ruby, Python, JavaScript, PHP, etc) is a norm, even for large and complex web applications. And in general, web apps tend to be complex.
I'm not saying that the languages are bad (well, PHP though… is a different story), however I personally use Python or Ruby only for short(-ish) scripts and things like that, when I want to get something done quickly. But when it comes to complex apps, static type system provides tremendous help. For example, when I try to compile some freshly-written (or refactored) module for the first time, I'd be very surprised if the compiler does not complain; most of the time there are some issues which I overlooked. So, if I didn't have the type system, I'd have to deal with these issues at runtime. This static analysis helps a lot to catch bugs early, so why not use that help?
Not only static types help compiler to catch bugs early, they also help reader to understand and navigate the code better. This is executable documentation, if you like.
People seem to agree that writing tests is a good thing, because it leads to better software. I really hope that sooner or later people will also agree that using statically typed languages also leads to better software (we even have TypeScript already!), and when it happens, Go looks like a very good candidate.
Of course, Go is suitable for many things other than the web; however it has everything one would need for backend web development: decent web server with http/2 support, sane parser/serializer for json, yaml, xml and such, etc. You can handle xmls and not go crazy!
But this topic deserves its own article (or a few of them).
What I dislike about Go
Not much. Off the top of my head, just a few things:
The flag package
First of all, not exactly about the Go itself, but its standard library: I said above that I do like the minimalism of Go, but some parts of it are below the threshold even for me. A shining example is the standard flag package; in my opinion, this is where people simplify too much. This package is just plain dumb. It doesn't distinguish between single-dash and double-dash flags (so --foo
and -foo
are equivalent), there's no way to make a flag required, no way to know whether the flag was given by the user or the default value was assumed implicitly, etc etc. I'd like flag parsing package to be as fashionable as Python's argparse, but, alas. Luckily, there are some third-party alternatives.
No generics
UPD: not relevant anymore. Since Go 1.18, go has generics! The remainder of the old section left as is anyway.
I don't have a pressing need for generics, but sometimes I do want to have them. FAQ says that they may be added to the language at some point, so I'm full of hope.
No way to enforce initialization of some struct's field
Consider a simple struct:
type Foo struct { a int b int }
We could initialize an instance of it like that:
f := Foo{a: 1, b: 2}
This is good and self-documented, but the problem is that when we add more fields to the struct, that initializer code will continue to compile with zero values being set for the newly added fields, and this is not necessary what I want. I'd like to be able to annotate some field to require explicit initialization of it.
That's not the end of the world, but still something to complain about, as I keep thinking of it rather often.
Alternatively we could use Foo{1, 2}
: in this case, adding new fields would force us to update initializer as well, but it's not self-documenting and error-prone in other aspects, so I tend to avoid that form.
GOPATH
UPD: not relevant anymore, GOPATH is deprecated by now. The remainder of the old section left as is anyway.
For the Go toolchain to work, source code must be located on a certain path, which is determined by the environment variable GOPATH
. This does cause inconveniences, which are unnecessary in my opinion. Luckily, this part is (hopefully) going to be solved:
Another future usability improvement that dep might enable, is allowing Go to work from any directory (not just a workspace in the GOPATH), so that people can use the directory structures and workflows they're used to using with other languages.
Conclusion
I worked with Go for a few years so far, and the more I work with it, the more I like it. That thing alone is quite something! A really uncommon one.
If you want to start learning Go, the Tour of Go might be a perfect entry point. For the more in-depth knowledge I prefer reading a thorough book; I personally love The Go Programming Language by Alan A. A. Donovan and Brian W. Kernighan.
And, of course, listening to Rob Pike talking about Go is very interesting and it gives you an idea of why things in Go are implemented as they are.
And as a bonus, a nice chorus by Scaleability: Write in Go
Happy Going!
You might as well be interested in:
Discussion
Perhaps this pattern will reduce pain: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
Thanks for sharing an interesting article; we tried to use this pattern but honestly the result looks way too complex for most needs, it almost feels like we're fighting with the language instead of embracing it. Enforced initialization and/or customizable default value could be better in most cases. But yes, sometimes even that “functional options” pattern is justified.
Why not use crystal if you already know and used ruby?
Crystal is too young so obviously the “big enough community” requirement is not satisfied, and it's an important one. Overall, Go feels much more mature and stable, so even though I do plan to play with Crystal in my spare time, but I wouldn't pick it for real life projects for which I have successfully used Go so far.
Not if you have good, automated tests…
See third party static analysis tools like RuboCop.
That said, I’ve been all-in on Go for 18 months for all the reasons you’ve summarizes!
Type system reduces the number of test cases I need, so I can focus on more specific tests instead of doing the job which type system could do for me.
Yes I know about these tools, and still I don't see the reasons to prefer them instead of a language which was developed with static types in mind from the ground up (other than having to support legacy code written in dynamic languages, but that's another story).
Glad to hear that!
One more thing I love – It's creators. When I heard Ken was developing Go I hopped right in!
if err != nil is poor design, there is no debate here. If you want items to fail implement the feature of Option[a] or Either[e, a].
Not sure I understand what you wanted to say. Fwiw, I'm not saying that
err != nil
is a poor design; quite the opposite.He is saying that
if err != nil
is a poor idea, and I have to agree with him. Much better way is to use Optionals or Eithers, as in Rust/Haskell/Swift/Kotlin/Scala/etc. This is also explicit (maybe even more than the Go approach) and compiler guarantees that you have to check the error instead of simply ignoring it. Also there is less code repetition, if you program in the Monad-like approach, where errors propagate easily.