Thoughts on Go and abstraction
I enjoyed reading Martin's take on why he thinks that Go is not an easy language and some of the follow-up discussion on lobste.rs. The title lead me to expect a much more click-baity article with many opinions, strongly held. Like so many times, I was lucky and my expectations were wrong. Martin leaves room for discussion and describes his opinions in a balanced manner that I appreciate and triggered me to write down my own thoughts.
To me, many of his arguments are about abstraction. He argues that a common task
like deleting an element from a slice could be as straight-forward as a single
function call. Go does not ship a delete
for slices and also does not (yet)
allow library authors to abstract over that task in a generic way.
Abstraction is an essential tool for any software engineer, but it comes at a cost. The lack of more abstract slice manipulation functionality, means that you often see a loop and indentation where you might expect a simple function call. To me, that has been a big benefit. Writing a loop explicitly, has often led me to consider the involved cost immediately. And seeing the boilerplate and indentation has often helped me to identify a bottleneck when trying to tune things. However, pushing this argument to the extreme would suggest that we should write assembly code, while the truth is somewhere in the middle between extremes. And that middle depends strongly on your use case.
I find it tough to argue that such an abstraction should not be included or at least be possible to add for library authors. I also agree with Martin, that adding generics is not a simple win-win situation as I've struggled to understand function signatures and their intended use in languages with with generics. Even without generics available as a language feature, there is a delete for maps but not slices. I would be keen to learn why it wasn't added for slices.
Martin also argues that combining Go's concurrency primitives to solve
real-world scenarios is not as simple as typing go
to start a goroutine. I
absolutely agree. In my experience this is due to the complexity of the issues
I'm trying to solve when I decide to go for a concurrent solution. Also looking
at this with abstraction in mind, it seems quite possible to abstract over the
task to limit the number of goroutines that run in parallel and then re-use that
abstraction.
Designing a perfect language is an impossible task. Considering immensely different use cases and then balancing the benefits and costs of adding core functionality to support them will never get everyone happy. There'll always be use cases that you decided to not support and different experiences that lead to different priorities when choosing a language.
Before using Go in detail, I enjoyed developing in Scala. Its design follows different principles from Go so that both have very different strong suites. In the context of the above arguments and in contrast to Go, Scala provides exhaustive functionality to manipulate collections and allows library authors to leverage generics. Whether the benefits of such a standard library are more important to us than some other strong suite of Go is up to us. We get to choose which benefits outweigh which costs when we get to choose a language for our project. At least most of the time 😉