I’ve been programming since the late 90’s and I’ve done quite a bit of coding in C, C++, a lot of it in PHP and some in Python as well. On the front-end I’ve done some JavaScript and I’ve also had the misfortune of programming in Java 😉
I started programming in Go in 2012 and since then I haven’t wanted to program in any other language. I’ve had a handful of large Go implementations across two companies and by now I have my own short list of favourite features.
One of those features is not mentioned very often but it has changed things significantly for me, and that’s what I’m going to discuss here.
The Good Features
Without further ado, here’s the rundown to my top features of Go.
Compiler & Syntax
In terms of convenience of programming, compiled languages have been a bit difficult to handle. Especially languages like C++ or Java that are almost impossible to write in without some kind of IDE Support.
Go on the other hand is very concise and simple. Things like type inference etc. make it very easy to code in – it almost feels like a scripting language. The error messages from the compiler are also very helpful, unlike, say, the C++ STL error messages.
The language syntax and conventions focus a lot on intent rather than expression. If you look at languages like Perl or Scala, you’ll come across many operators or syntactic sugar to express some logic, which is not the case with Go. While it’s very nice to write with those operators, it makes things very difficult to read later on. Go uses very straightforward imperative syntax – just clearly state what you intend to do with a few keywords and operators.
Another interesting aspect about Go is that if the programs compile, they usually work. This is one of the properties of well designed languages. I first observed it with Python, where if I wrote a program it usually worked correctly the first time more often than it did with C or C++ or PHP. This reduces the time to finish quite a lot.
Lastly, the performance and reliability you get from a Go program in the first hour or day of effort is phenomenal. Getting the same level of performance in, say, Java would take hours to optimise the code and figure out the features to use, etc. That makes Go a highly productive language.
Small Feature Set
Go is only slightly bigger than C, and C is a very concise language. I remember when I was in college and I had to appear for a C programming exam. I read the Kernighan and Ritchie book cover to cover over the weekend and aced the test. That’s not something you can do if you want to learn C++ or Java or most other mainstream languages.
Because it’s small, Go is also very easy to learn and remember. All the features that are present in Go are orthogonal and minimal. Orthogonal means that you can combine any feature with any other feature in meaningful ways. So the number of things you can do with a small set of features is very large because the number of meaningful combinations of features is large. Minimal means that there are not too many different ways to do the same thing. This is unlike the Perl philosophy of TIMTOWTDI (there is more than one way to do it). In Go, as in Python, there’s only one good way to do something.
The net result of this is that there are very few “knowledge islands”, unlike in large languages. With large languages like C++ or Java, you will find many programmers that are only familiar or comfortable with a subset of the language. There are very few people who know the entire language end-to-end. “Knowledge islands” make it very difficult for people to exchange ideas with each other if there’s insufficient overlap in knowledge between them.
Standardised Formatting
This is one of the most famous features of Go and it improves accessibility of code by an exponential factor. That in turn improves collaboration. When you see your code vs. your team member’s code it looks exactly the same. When you see code written in your team vs. code written in another team, it still looks the same. This reduces the psychological “ours” vs. “theirs” cognitive barrier and one of the effects of this is in making open source code more accessible.
Sensible Unicode & Strings
This is another feature I like a lot, though it doesn’t get talked about much. Go is one of the very few languages that get Unicode and strings right.
One of the things that I did back in 2003 was to implement a binary XML syntax – a more efficient XML serialisation format – and I enjoyed using C++ strings library a lot for that. I could read bytes directly off a file and use all the string manipulation features to deal with them. That’s something I found missing in Java.
In Java if you want to do any kind of string manipulation – say, parsing the first few bytes of a header or do substring matching , etc. – you would first have to convert the bytes into strings. And strings are Unicode code points in Java. One of the consequences of which is that it’s not just inconvenient to program with, it’s also inefficient. Whenever you need to transform the bytes into strings, you also necessarily have to do a memory copy because the underlying data types are incompatible.
Go very cleverly uses UTF-8 representation of a string as the basis of its string
type instead of Unicode code points. Perhaps it’s because Rob Pike is the inventor of UTF-8, but it’s a very good design decision anyway. Since UTF-8 ubiquitous, it makes writing network programs a lot more convenient compared to, say, Java – or Python 3 for that matter. I don’t know why Python 3 went the Java way.
Channels & Goroutines
Another headline feature of Go is, of course, Channels and Goroutines. It can be argued that Go did to concurrent programming what Java did to memory management – make the respective task significantly safer and simpler in an industrial strength programming environment.
Go’s concurrency model is based on the formal theory of Hoare’s CSP (Communicating Sequential Processes) Model, and allows writing concurrent programs in a more declarative manner. Simply preceding a function call or method invocation with the go
keyword causes it to execute in a concurrent context.
Channels allow unidirectional or bidirectional data exchange between goroutines with built-in blocking primitives for synchronising between senders and receivers. Unlike NodeJS or async Java frameworks, coordination through channels enables concurrent logic without the proliferation of callbacks, which become very difficult to understand and reason about as the size of code grows.
select
Statement
The select
statement is, I think, the party piece of Go’s concurrency features. It is a very simple, declarative way to combine multiple blocking events (channel reads or writes) and branch off some logic based on which of the events unblocks first.
It allows writing some of the most difficult concurrency patterns in an easy to understand and safe manner. Timeouts & cancellation, back-pressure, worker pools, etc. are easy to implement. Even more sophisticated synchronisation and scatter/gather logic is made possible with simplicity using select
.
Intermission
With all of these wonderful features and some more that I’ve not even covered, Go permanently altered my programming capabilities and the kind of programs I wrote.
The first production system I wrote in Go was a real-time multi-player game that earned over a million dollars in a year and ran on just one server, the other one being a warm stand-by.
Over the years I also developed/co-developed a user activity rate limiter, a reverse proxy, a micro-service simulator, a deployment orchestrator, an auto-scaler and even a bespoke datastore!
The best feature of Go, however, changed the way I program.
This feature that I’m going to talk about next acts as a stand-in for the user so you can code from the mindset of a user rather than a programmer. It improves documentation. It can be a very significant guide for improving the design and modularity of your program. It improves the speed and reliability of refactoring and debugging. It finds unnecessary or dysfunctional code, and it gives rapid feedback on performance characteristics of your program as it is being developed.
Can you guess what this feature is?
go test
Most people already know of go test
as a built in unit testing framework that comes as part of a standard Go installation. Which, by itself, is a pretty significant improvement over many other languages where you need to make your own choice of a unit testing framework and then worry about making it work with the rest of the ecosystem like editors, build tools, reporting tools, etc.
However, there’s more to go test
than meets the eye. Let’s look at what all go test
can do for the programmer.
Design Phase Assistance from go test
The first point where go test
can facilitate program development is by simulating the user of the program. Most of the time, when we start writing a program, we write a main
function to “try out” the program. A better way to do this in Go is to create a test file. If you’re writing a package mypkg
, the test file should declare itself as package mypkg<b>_test</b>
. This makes the test file an outsider to your package, so you need to import your package into the test file to access its functionality.
This little trick immediately allows you to switch roles between a developer and a user. It can guide your API design. It can help you decide what symbols to export and what symbols to keep private. By ditching main
for a test package, you take the first step towards integrated testing. And, as they say, getting started is half the job done!
Another significant way that go test
helps in program design is by forcing you to think about ease of testing. Monolithic, do-it-all functions are hard to test, so balancing your urge to implement with the need to test naturally leads to improved modularity and improved cohesion. As an example, when developing a micro-service, I typically use the following strategy:
Implement all of the business logic as native APIs with native data structures — this is the package whose unit tests exhaustively cover business logic testing
Implement data load/store from native data structures to database — this package is devoted only to data handling and the unit tests only cover data transformations
Implement RPC (HTTP etc.) as a wrapper using the previous two packages — for this package the tests only deal with request interpretation and response serialisation, not business logic
Lastly, go test
offers an excellent way to write example code that shows up in the go doc
documentation at the appropriate places and is verified for correctness. If you want to document a function, just write func ExampleFunction() {…}
in a test file and this code will show up next to the documentation of the function, Function
. Similarly write func ExampleStruct_Method() {…}
to document a method, Struct.Method()
. If you want to document a use case, write func Example_useCase() {…}
to document an entire use case. Adding a block comment starting with Output:
at the end of the example’s implementation will make go test
execute the example and match its output to the comment so you know whether your example works. go doc
would document the output as part of the example.
Implementation Assistance from go test
If you write some straight-forward table driven tests for your package’s public API, those tests act as your compatibility guarantee while you’re refactoring things around. If they pass, your refactored package is still working as expected. You can organise your test functions into top-level tests that test out scenarios and have subtests within them to test more granular functionality.
If, after a refactoring change, your tests don’t compile, it’s a clear indication of changes in the package APIs. If the tests compile but fail, it indicates a change in functional behaviour of your package. The more granular the tests, the easier it is to locate the faulty change.
go test
not only helps with ensuring functional correctness but it also helps in verifying non-functional aspects. If you write concurrent tests and run the tests under a race detector ( go test -race
), you’ll be able to catch any data races that, if left undiscovered, can cause your program to crash while it’s running in production.
It is also possible to write some long running tests to verify deeper behaviours of your implementation. For example, my package smartcb has a few long simulation tests that don’t execute by default but get enabled when invoked as go test -tags sims
.
It is even possible to use go test
to discover code bloat. Usually, you would run go vet
and among other things, it will find unreachable code for you, that you can simply delete. However, this doesn’t cover all situations. One excellent property of writing exhaustive unit tests is that if you get a test coverage report and it’s not 100%, you are either not testing your code thoroughly enough or you have some code that is not testable because it represents a logically impossible scenario. This is a very powerful way to discover unused functions or types that otherwise keep accumulating as a piece of code goes through multiple maintenance cycles.
Performance Optimisation from go test
One of the most powerful features of go test
is the benchmarking infrastructure. The ability to monitor the performance characteristics of code as it is being developed and assess the impact of code changes on performance is a goldmine.
Just write a few benchmarks for top-level package APIs in your test files. Then, to find how fast the functions/methods are, run:
go test -bench
To observe multi-core scalability, with various values of N, run:
go test -bench -cpu N
To see what parts of your code are slow, run:
go test -bench -cpuprofile
To see what parts of the code eat up memory, run:
go test -bench -memprofile
Run go test -bench
after every commit to find performance regressions if you are writing something performance sensitive.
Think that’s something too hard to do? Think again. Here’s a video that shows just how long it takes to test a non-trivial program, benchmark it for performance and analyse the benchmarks for performance issues.
Closing Thoughts
Having experienced the power of go test and its influence on my programming practice, the following statement sums up my view about it.
Go test is like a programmer’s assistant. An Ironman’s Jarvis. A Batman’s Alfred. It happens to run unit tests too.
— Yours Truly
This post is based on my talk at Tokopedia Tech-a-Break. Following are the accompanying slides.
The Best Feature of Go – A 5 Year Retrospective from Tahir Hashmi