The lure to use channels is too strong for new users.
The nil and various strange shapes of channel methods aren't really a problem they're just hard for newbs.
Channels in Go should really only be used for signalling, and only if you intend to use a select. They can also act as reducers, fan out in certain cases. Very often in those cases you have a very specific buffer size, and you're still only using them to avoid adding extra goroutines and reverting to pure signalling.
If you take enough steps back and really think about it, the only synchronization primitive that exists is a futex (and maybe atomics). Everything else is an abstraction of some kind. If you're really determined, you can build anything out of anything. That doesn't mean it's always a good idea.
Looking back, I'd say channels are far superior to condition variables as a synchronized cross-thread communication mechanism - when I use them these days, it's mostly for that. Locks (mutexes) are really performant and easy to understand and generally better for mutual exclusion. (It's in the name!)
Go's design consistently at every turn chose the simplest (one might say "dumbest", but I don't mean it entirely derogatory) way to do something. It was the simplest most obvious choice made by a very competent engineer. But it was entirely made in isolation, not by a language design expert.
Go designs did not actually go out and research language design. It just went with the gut feel of the designers.
But that's just it, those rules are there for a reason. It's like the rules of airplane design: Every single rule was written in blood. You toss those rules out (or don't even research them) at your own, and your user's, peril.
Go's design reminds me of Brexit, and the famous "The people of this country have had enough of experts". And like with Brexit, it's easy to give a lame catch phrase, which seems convincing and makes people go "well what's the problem with that, keeping it simple?".
Explaining just what the problem is with this "design by catchphrase" is illustrated by the article. It needs ~100 paragraphs (a quick error prone scan was 86 plus sample code) to explain just why these choices leads to a darkened room with rakes sprinkled all over it.
And this article is just about Go channels!
Go could get a 100 articles like this written about it, covering various aspects of its design. They all have the same root cause: Go's designers had enough of experts, and it takes longer to explain why something leads to bad outcomes, than to just show the catchphrase level "look at the happy path. Look at it!".
I dislike Java more than I dislike Go. But at least Java was designed, and doesn't have this particular meta-problem. When Go was made we knew better than to design languages this way.
In reality, you've just made your code unpredictable and there's a good chance you don't know what'll happen when your buffered channel fills up and your code then actually blocks. You may have a deadlock and not realize it.
So if the default position is unbuffered channels (which it should be), you then realize at some point that this is an inferior version of cooperative async/await.
Another general principle is you want to avoid writing multithreaded application code. If you're locking mutexes or starting threads, you're probably going to have a bad time. An awful lot of code fits the model of serving an RPC or HTTP request and, if you can, you want that code to be single-threaded (async/await is fine).
1. send a close message on the channel that stops the goroutine
2. use a Context instance - `ctx.Done()` returns a channel you can select on
Both are quite easy to grasp and implement.
If you run a microbenchmark which seems like what has been done, then channels look slow.
If you try the contention with thousands of goroutines on a high core count machine, there is a significant inflection point where channels start outperforming sync.Mutex
The reason is that sync.Mutex, if left to wait long enough will enter a slow code path and if memory serves, will call out to a kernel futex. The channel will not do this because the mutex that a channel is built with is exists in the go runtime - that's the special sauce the author is complaining doesn't exist but didn't try hard enough to seek it out.
Anecdotally, we have ~2m lines of Go and use channels extensively in a message passing style. We do not use channels to increment a shared number, because that's ridiculous and the author is disingenuous in their contrived example. No serious Go shop is using a channel for that.
Like closures, channels are very flexible and can be used to implement just about anything; that doesn't mean doing so is a good idea.
I would likely reach for atomics before mutexes in the game example.
I think that was one of the successes of Go
Every big enough concurrent system will conclude sync primitives are dangerous and implement a queue system more similar to channels
Mutexes always look easier for starters, but channels/queues will help you model the problem better in the long term, and debug
Also as a rule of thumb you should probably handle panics every time you start a new thread/goroutine
Is it worth learning it? What problems are best solved with it?
Go implementations of CSP somewhat mitigated the problems raised in the book by supporting buffered channels, but even with that with CSP one end up with unnecessary tasks which also brings the problem of their lifetime management as the article mentioned.
Still, this example has other issues (naked range over a channel?!) potentially contributing to the author’s confusion.
However, this post was also written almost a decade ago, so perhaps it’s a result of being new to the language? If I cared to look, I’d probably be able to find the corresponding HN thread from that year full of arguments about this, hah.
If you don't understand how to use channels, you should learn first. I agree that you might have to learn by experimenting yourself, and that
a) there is a small design flaw in Go channels, but one that is easily fixed; and
b) the standard documentation does not teach good practices for using channels.
First, the design flaw: close(channel) should be idempotent. It is not. Is this a fatal flaw? Hardly. The work around is trivial. Create a wrapper struct with a mutex that allows you to call Close() on the struct, and that effects an idempotent close of the member channel. Yes this is a bit of work, but you do it once, put it in a re-usable library, and never bother to think much about it again.
b) poor recommended practices (range over channel). The original article makes this mistake, and it is what causes his problem: you can never use range over a channel in production code. You must always do any select on a channel alongside a shutdown (bailout) channel, so there will always be at least two channels being select-ed on.
So yes. The docs could be better. It was immediately obvious to me when I learned Go 12 years ago that nobody at Google ever shuts down their services deliberately. Fortunately I was learning Test Driven Development at the time. So I was forced to figure out the above two rules pretty quickly.
Once those two trivial fixes are in place, Go sails. There are Go libraries on github that do this for you. You don't even have to think. But you should.
Handling errors is only as verbose as you want it to be. You do realize you can call a function instead of writing if err != nil so much, right? Sheesh.
Go _is_ something of a sharp tool. Maybe we need to put a warning on it: for mature audiences only.