As a means to better understand Kotlin’s coroutines by way of my prior understanding of Go’s goroutines, I wrote gotlin which reimplements the goroutine examples from The Go Programming Language book in Kotlin with coroutines.
In no way do the authors of “The Go Programming Language” endorse this or this use.
This post assumes you’re at least vaugely familiar with Go, goroutines, Kotlin, and Kotlin’s coroutines. If you’re not, here are some resources before diving in:
Coroutines can be challenging to understand, leading to potential issues when used incorrectly. I cannot cite examples here but I have reviewed the severe event retrospectives where, for example, the root cause was improper use of
runBlocking within another
runBlocking. This has lead some engineering orgs to have an irrational fear of coroutines and of Kotlin as a language as a whole. Concurrency is hard, but it’s not impossible and should be understood, not feared.
Because of this, I set out to better understand coroutines myself and I struggled as well. While watching a talk that demonstrated the use of Kotlin’s
select it clicked that part of my misunderstanding of coroutines is that they’re very similar to Go’s goroutines. I was treating them too similarly in my head. While at the same time, I didn’t have many (any?) great practical examples of coroutines in use.
I set out to better understand coroutines by directly comparing them to the goroutine code examples in The Go Programming Language book. While I only occasionally use Go professionally, I think this book is absolutely top-notch; easily one of the best programming language books on my shelf.
Granted, this is targeted at a pretty niche category:
- Developers who know Go and its goroutines reasonably well
- Also don’t know Kotlin’s coroutines reasonably well
That said, the way to use it is:
There are more details in the gotlin README but here are some higher level things I learned
First, just like in grade-school, let’s compare and contrast the two:
- Both provide/are a super lightweight alternative to Threads
- Both support sharing memory by communication (rather than communicating via shared memory)
I think this is the key point: Many of the differences between goroutines and coroutines come down to philosophical design differences between the Go and Kotlin programming languages.
Go / goroutines
Go emphasizes simplicity, practically above all else. For example, its runtime has a garbage collector, but very few ways to tune it. Of course it has for-loops, but no functional equivalents of map/reduce/filter. Go’s philosophy seems to be to prefer to remove what can be removed from the language and only add features when there’s been a clear demonstrated need over time. This is in stark contrast to many languages that absorb features quite easily, like C++. Which of course makes sense given that it was born out of dissatisfaction with C++. This explains why it took so long for Go to support Generics.
Back to goroutines, this minimalist philosophy shows up in how goroutines work:
- Goroutines baked into the language via
go, as are channels. This allows them special in/out channel syntax and
- Goroutines are unmanaged. Go doesn’t give a way to cancel them and will let you exit your program without closing out goroutines.
Kotlin / coroutines
On the other hand, Kotlin is anything but minimalist. I’d consider Kotlin a maximalist language with pragmatic defaults. Kotlin tries to provide what’s likely best for you in short reach (e.g. collections are immutable by default) but lets you run with whatever paradigm you’re most comfortable with among imperative, object-oriented, functional, or declarative (via DSLs). It leans toward immutable functional-spiced value-data-oriented programming, but it doesn’t impose a philosophy unto you. Much like its ancestor, Java, it provides managed functionality.
Along with that philosophy in mind, some distinctions around Kotlin’s coroutines are:
- Coroutines are provided via a library, kotlinx.coroutines, and not within the language itself. It’s a testament to Kotlin’s flexibility that it can just bolt-on coroutines like this.
- This “bolt-on” approach does mean there has to be a bridge between “normal” Kotlin and “coroutine” Kotlin. This is
runBlockingand it’s important to not step through this bridge twice!
- This “bolt-on” also results in limitations, e.g.
selectcannot have clauses send and receive from the same channel.
- Kotlin’s coroutine library also provides some pre-built common patterns, unlike Go which expects you to build them yourself:
I took on this project with two goals, both intentionally limited in scope:
- Better understand Kotlin coroutines so I can use them effectively
- Hopefully provide a resource to help others also better understand
I will have to wait and see if #2 is met, but I definitely met my #1 goal.
A large part of that was due to the very helpful expert PRs from dump247. Going into this, I expected it to be a self-study, code, then release process. I tend to run solo and I never really understood “study groups.” I didn’t even consider or expect anyone else to contribute, but I’ve learned more from his contributions than I would’ve on my own.
I’ve left room for additional enhancements.
Suggestions and pull requests are welcome! I continue to learn and welcome any constructive input.