When to use or not use channels with goroutines
Channels → in Go are synonymous with go { routine(s) }
However this unfortunately has some developers believing that, whenever you use a goroutine, you must use channels.
This short blog attempts to diagrammatically explain when one should and should not use channels in go.
For the sake of comparisons to other languages that rely on the term thread rather than what we call goroutine, we will refer to them as Activity, for the sake of neutrality.
When to use channels
There are plenty of articles that already support the use of channels and how they are used, so we wont get into that. But lets start the famous go quote…
Do not communicate by sharing memory; instead, share memory by communicating.
So far, so good. But why is this preached?
synch {
We need to understand that shared memory, is difficult to manage in concurrent models. The onus on protecting (or synchronizing) memory access between competing activities, lies on the developer. Ideally if you can get away by using a Mutex, that is great.
In languages like Java, if you find yourself dealing with high level synchronization of large blocks of code or function, or multiple synchronized blocks inter dependent on each other then you are in trouble!
}
Goroutine
A goroutine, can be achieved by putting the go
keyword in-front of an ordinary function; sayf()
. By design, your function should look the same irrespective of it being called as a go routine or not. This is because in most cases you are providing concurrency to the function. This implies the function itself is running in a concurrent context provided by the goroutine.
When developers write goroutines, they forget that a it is nothing but a function. Coupling a function always to a channel is a prevalent and negative practice. The reason I call it negative, is because how a function can be called with our without a concurrent context is a secondary concern only influenced by what it wishes to communicate with another function. In other words, channels imply coupling between functions, and hence increases design constraints. This complexity must be worth the cost.
When not to use channels
There are two cases of memory sharing that are are thread safe.
Immutable type
go makes no guarantee about immutability; it can be implemented but here we are referring to the idea that state cannot (should not), be changed. It also explains this in the light of other languages where immutability is supported with concurrency in mind.
Immutable types (across languages and not just specific to go), are inherently thread safe. Since they cannot be written to and are intended only for read purposes, they afford to bravely sit in a shared space and be passed around without worrying about the effects of concurrency on them.
Disclaimer: If your immutable is part of a mutable type, then this does not hold true, as there is still an aspect of mutability.
Private items on a stack
Above each activity maintains its own stack of `[x1, x2, x3]`. There is no requirement to share or pass these, and each stack maintains its own copy as it calls functions. It would further help if x
was not mutable.
Asynchronous event handlers
Some developers loosely use channels, for even situations where concurrency is not needed; where one can use event handlers and callbacks.
Conclusion
Channels also do come at a cost, both in terms of performance as well as design complexity. Furthermore channels couple goroutines, and coupling introduces complexity, which in turn reduces re-usability. Use of channels should be given more thought.