Note: This is a continuation of this article
Type safety and the compiler to the rescue
One of the first things we noticed using Go was that once a program compiled, it tended to work.
This was not the experience that we had in Python. Small programs usually took a lot of runs before we got the bugs out. But Go usually just worked.
Now of course, larger programs are different, Go isn't magic. And Go does have a few runtime problems, namely nil pointer dereferences, non-initialized channels and assigning to a nil map. Python seems to have an infinite number of these because of when evaluation is done and its dynamic nature.
An early project where a service was re-written from Python to Go and run side by side showed us immediate advantages. Much lower memory use, faster response times, and significantly higher loads. And remember, still pre-Go 1.0 (again, we aren't using multi-processing in Python).
It also just worked out of the gate, something we didn't see from our Python code. I chalk this up simply to type safety.
Goroutines
The idea that threading could be simple had been completely lost on me. After years of deciding on which pain pill I wanted to take, inheriting from threading.Thread or running the thread via some archaic method? Do I need thread local data, is it going to be a daemon thread, ... I got this instead:
go func() ... done
Want a promise?
ch := make(chan bool, 1)
go func() {
...
ch <- true
}()
...
...
promiseKept := <-ch
This was certainly easier than what Python provided in terms of threading/mulit-processing. It was first class within the language, not as a standard library and that made all the difference.
And even better was how cheap a goroutine was when compared to a thread or a fork call (even with all of linux's optimization, if your on linux).
Objects without inheritance, mostly...
What I learned about objects over the years is that people get way too excited about them after they learn how they work. I'd look through code that inherited from three classes, each which inherited from two other classes, that inherited from ...
When your trying to dissect someone's code, this kind of thing can drive you mad. And if I was running into this at Google, I've got to imagine it isn't much better in other places.
Go does composition, which has similar traits to inheritance. I've noticed that composition tends to be used sparingly and in ways that are easy to understand. I think that might be because it is not presented as something that the language design is based around, so it might not get as abused, but I don't really know.
This isn't a language "feature", but whatever it is I appreciated it. People often argue that when someone is using X feature wrong that it is not a problem with the language. I think that if you are seeing a feature used incorrectly a lot, it is a fault of the language.
Interface contracts, not object method overloads
Interfaces as contracts just made so much sense, or at least once I understood them. They were the most mysterious part of the language for me at first.
In comparison to how I would see it done in Python, this was better thought out. In Python, what I saw most often was creating a base class that had methods that subclasses would overload. But there were no guarantees here, just crashes when something wasn't implemented.
The compile time constraints around interface contracts prevented a lot of bugs and made code more readable.
No mocks, just interfaces for tests
I didn't do a lot of programming in Python outside the Google sphere, but one of the main methods of testing used internally was mock libraries.
I really hated every mock library we had. I found the tests hard to read and very brittle. And to get our code to work, we were always trying to get 90% coverage (by our definition of coverage, lots of coverage definitions) in order to stave off runtime bugs.
This often made code changes break lots of tests. Engineers were spending more time trying to fix the tests than working on the code.
Go didn't have a mock library when I started using it, though one did come out within a couple of years. You simply used private interfaces for object attributes, which allowed you to switch out for a fake at any time.
Fakes were generally easy to write and self-explanatory. Along with the table driven test method, it made reading tests much easier.
Because tests weren't as brittle, we would spend far less time debugging test breakage, except when we had actually broken something.
To be fair, you can certainly do fakes in Python. And you can do table driven tests with named tuples (I started doing this). But this wasn't the culture, which is sometimes as important as the language. And there was nothing that was going to make the amount of testing lessen, type safety just isn't there to pick up the slack.
Reflection
Python makes reflection very easy. Objects are really dictionaries and its easy to do runtime reflection any object.
Go has built-in runtime reflection, but it's not the easiest thing to learn. I remember thinking that I'd come across an alchemist cookbook for making gold, with the book cover in English and the contents in Greek.
Much of that difficultly, I believe, is because of the nature of a compiled language that doesn't want all code bogged down by the runtime.
However it was a compiled language with reflection and introspection. That was pretty awesome.
But if there was something Python did better than Go, reflection was it, hands down.
Search and replace
Have you ever wanted to replace a variable name across all files in a directory? Or worse, want to change an object name across all files in a repository?
In Python, there was just no end to the bugs. We'd spend forever tracking down all the issues.
Go provides out of the box tooling for doing this work. And it just worked.
Better yet, Go provided the AST library for building your own tools. The negative was, there was no good documentation. Today the documentation situation is slightly better with some blogs giving examples, but the godoc is severely lacking. But at least there is a way to do this.
Does not support runtime attributes
Python has this annoying or powerful feature, depending on what viewpoint you are coming from: adding runtime attributes to object instances.
The problem with adding runtime attributes is that spelling errors are now problematic. If I assign a value to "object.supercalifragilisicexpialidocious" instead of "object.supercalifragilisticexpialidocious", Python just creates a new attribute and assigns the value.
My nice web application isn't displaying what it should, but I'm not sure where the problem lies in my code. Worse, I'm dependent on my eyes seeing this misspelling.
In Go, you cannot add an attribute at runtime to a struct, you will always get a compiler error:
b.hello undefined (type blah has no field or method hello)
Do it our way
One of the things that Go is known for is being very opinionated. This sometimes really upsets people. They've done things with methodology X and they don't like being told that Go isn't going to work that way.
I think all humans like the illusion of choice. We want a myriad of options. But we are often confused when there are a lot choices.
When I'm programming, I don't want to be the guy at the restaurant who is looking at the menu for an hour trying to decide what to eat. I want to go to the restaurant that has only one thing on the menu.
Go seems the right fit for me here, where I think Perl was the opposite extreme. I could never read another engineer's Perl code, because there were so many options to choose from, I was always seeing new calls I had never seen before. The tedium of looking up every other call bogs you down.
Go was easy to read because there was a limited way of doing things. And each method felt like the Go authors took a lot of time thinking and experimenting on what was the best way to do something. It felt well curated.
At first the "do it my way" irked me a little, but after a while I really appreciated this "feature".
Must use imports and function variables
Go has this really annoying feature when I started to use it, must use imports. I would get so irritated when I'd need to put in debugging statements only to find that I had deleted the log import because the program wouldn't compile, because it wasn't used.
But that quickly faded as I figured out the benefits. Those huge binary sizes in my Python files were sometimes causes by imports that weren't needed. We of course got better with linters, but you can still fool Python's linters in ways Go compiler won't let you. Go keeps your code neat by forcing you to get rid of variables you're not using. You can't skip around this because your annoyed or want to get something done faster. Which is great for code health.
With the inclusion of goimports in the toolset, adding/removing of imports was no longer annoying. My editor could now just add or remove my log import whenever I saved a file.
Performance
When it came to memory performance, Go won hands down. I remember the number was around 4x the size for basic types in Python vs Go. I've heard Java also has a similar type of memory consumption once a type becomes an object, but I've never verified that claim.
With Goroutines vs. the GIL, Go was the clear winner. Remember, multi-processing wasn't an option for us at the time. But even if it had been, it's just not as convenient as a shared memory threading model.
I certainly watched micro-benchmarks of Python beat Go. Especially in code where what is tested is really optimized C code. But once we left the micro-benchmarks, Python just wasn't holding up.
Sometimes these micro-benchmarks were for things like regexes. I'm sure Python's re library still beats Go's regexp package. But it didn't account for things like bounded memory consumption and constant runtime that Go's regexp provides and re does not (regexp is based on RE2 by Russ Cox).
In the end, we found the realtime performance of Go was multiples of Python (again, no multi-processing) with memory consumption far less. In modern Go, I imagine this gap is getting wider, though we no longer have multiple implementations of the same systems for realistic benchmarks.
This was a language that could be taught to novices
Python was a language that could be taught fairly easily to people who had never programmed before. One of the problems with C++ was that this was not the case. There is a reason dynamic languages have picked up over the years, with one of those being that they are far easier to learn.
We had a few people in a remote office coding at least part-time in Python. And within the local group in Mountain View, we had a lot of people with at least passing familiarity with Python.
These were not hard-core software engineers. Their main job wasn't coding, it was running a network. So any language that we switched to would need to be easy to learn.
I've taught classes in both Python and Go across the globe. I've held office hours to help with coding issues.
What I found was that Go only had two subjects that made it harder to learn than Python: interfaces and pointers.
This meant that once you could convince people this was the way to Go, you could train them fairly easily. The trick of course is convincing people, but that is a whole other article.
The end result
Certainly from the article's title, you can tell I switched to Go. I tend to think, with very little data, that I am about 6x as productive.
One project that myself and a team of SWEs wrote in Python took about a year. And it had a lot of performance issues we worked on for a year following that development.
I re-wrote that project in Go, by myself, with many feature enhancements in about the same time. The new service has about 1000x the usage with no signs of similar performance issues.
While it took about 4 years, I was able to migrate our engineers out of Python and into Go. The tools group took a hybrid approach, with half the projects in C++ and the other half in Go. I like to think some of that was my influence.
Today our situation looks quite different. Engineers are concentrating on the problems they want to solve, not performance issues or random crashes.
Is Go for you?
This certainly isn't something I can answer. It might be the scale of your projects doesn't have the needs we had. Or Go, might not have critical libraries you require.
Go certainly might not be the answer for every programming project. Sometimes you might need the speed that only highly optimized C++ or assembly can give you. Or maybe something like Rust is a better fit.
But if you're a Python/Ruby/Node programmer, I suggest giving Go a chance. Write 5k lines with code reviews from someone knowledgable about Go (it's not just a language, but like all languages requires a certain mindset to truly utilize it). 5k lines, because you have to get over the: "why doesn't it do x, I don't like y" mental game we all do when picking up a new language.
My guess, you won't want to Go back (pun intended).
Happy programming ladies and gentlemen, in whatever language you love!