Simplicity is one of these words that get thrown in arguments between developers to defend one approach against another. However, the meaning of the word seems to differ depending on who’s trying to make a point.
This came up recently in a conversation with Mike Perham & Yehuda Katz about Go. Someone mentioned that at Splice we’re particularly happy with Go’s system resource footprint, and the conversation veered into one about simplicity.
Looking at the dictionary’s definition here is what is says: “the quality or condition of being easy to understand or do“. Understand or do, not understand and do.
Rails makes some tasks easy to do, but very hard to understand. In Go, on the other hand, implementing complex logic may require a few extra (albeit simple) steps, but the result is easier to understand. Depending on what you value the most, you might argue that Go is simpler than Rails, or that Rails is simpler than Go. Technically, they are both simple per the dictionary’s definition, but the design choices the technologies make have drastically different ramifications.
Do you prefer to pay upfront or get a credit line?
I have to admit that when I chose to use Go as the core language for Splice, I didn’t primarily choose it because of simplicity or maintenance. Looking back, I was trying to decide between Scala and Go. But I did notice that maintaining a consistent code approach in Scala was way harder than in Go since Scala was letting the developer to freely choose when to use an OO or functional approach, and immutability wasn’t really enforced. Go doesn’t give you much style flexibility to the developers. It ships with tools like go fmt and goimports. Fast forward a year, we’ve written a lot of Go production code and I’m still very happy with my choice.
How does Go implement simplicity?
Make some decisions on behalf of the developers
The Go team chose early on to purposely not give developers the freedom they might demand. All general programming problems should have a unique and consistent solution, the idea being that developers should spend more time addressing their problem domain issues, not finding the best way to use a language. This is also why Go is garbage collected—developers shouldn’t be forced to deal with memory management. Reduce the number of available options and developers will more rarely shoot themselves in the foot. It does make the language a bit boring, but that’s probably a good thing. Get excited about what you build, not what tools you use to do it.
Logical and consistent approach
Most technical problems should have a unique solution. The Go team knew it was important and are doing a great job showing what they think are the right solutions. A good example is effective Go, a document designed by the Go team to explain idiomatic solutions to common tasks. A couple other examples rooted in the same approach: go fmt, a tool that automatically formats your code to follow the convention, or the amazing proposals discussed publicly before implementing something new.
Avoid complexity pitfalls
There are things that are known to increase complexity as a code base grows: inheritance, meta-programming, memory management… Go simply chose to find simpler alternatives. That’s also the reason why generics aren’t supported. While these decisions often fuel a passionate debate between programmers, they are all rooted in the same value: avoiding complexity.
Do NOT defer complexity cost
Nothing is free in this world. Flexibility at the language level moves the cost of complexity to the developer. Go developers are fine with the lack of flexibility, they have a tendency to prefer to pay the price up front and not have to worry about it later on. The learning curves for Ruby and Go are very different. Ruby—and especially Rails—tries to abstract anything that could prevent anyone from getting started. Go, while simpler, requires developers to understand programming at a deeper level. It forces developers to think about architecture. While a language such as Ruby often embraces an implicit approach, Go leans towards explicitness. There are a few exceptions such as inferred types (if the compiler can guess the type, there is no need to declare it), reflection, naked returns, and a few others, but overall, the idea is that you pay a certain price up front and won’t pay it later on in maintenance. At Splice, we learned that the upfront cost is actually very low and certainly cheaper in the medium term.
Go’s standard libraries are exemplary. They provide a great place to learn by example and they are very useful. Most projects don’t need 3rd party resources. At Splice we use only a handful external packages. Because the standard libraries are written by the core team and designed the same way as the language, there are very few surprises. Rarely do you feel the need to look for a lib doing something you can’t do yourself. Not having to rely heavily on 3rd party libraries keeps unneeded complexity from creeping in. Libraries don’t become stale or unmaintained. A good example of why having unified standard libraries is very well explained in this very well written article about Go’s io.Reader.
Fast compiler / static types
Go’s compiler is fast, really fast. It’s actually much faster for me to compile and start our Go code than to boot our Rails app. This means that I can hook up the compiler in my development process. Every time I save a file, the compiler runs to find issues and reports right away. Anybody working with statically typed language know the benefit of it, but often the cost of that is significant compilation time. I’ve been a big advocate of dynamic languages for a long time, but I have to say that having a compiler finding my typos and obvious errors gives me a feeling of security and more robust code. I’ve found that in Ruby I often wrote tests that were doing things a compiler does for free. With a fast compiler the tests are also really fast, which is a big help when refactoring.
Refactoring is a key part of maintenance. I’ve yet to see a great developer maintain code without refactoring things over time. Go’s package approach and interfaces makes refactoring way easier. I think it boils down to architecture: Go packages and interfaces force you to think about isolation, composition and reusability. Other languages also have great ways to namespace code and provide composition, but Go’s interface approach feels lighter, more flexible, and more robust than the free-for-all dynamic language approach. I think this calls for Katrina to write a blog post on Go and refactoring 😉
Growing a team
The final thing about Go simplicity and maintenance is growing a team. When you work in a startup, you know/wish the team will grow fast. You want to be able to add people and see them contribute quickly. By having opinionated and well-defined conventions, architecture decisions surface quickly. As a new developer you don’t have to learn the company’s code style, just get to heart of it. The first Go developer we hired was Martin, and I loved how on his first day, he took apart my code and immediately pointed out issues and nitpicks and refactored everything while learning the problem domain.
In conclusion, Go isn’t a silver bullet, other languages probably provide the same value and Go definitely also has its cons. But Go’s approach to simplicity is the right balance for us. It’s pushing us to make better architectural decisions while still delivering value at least as fast as if we were using other languages but with a lesser maintenance cost.