Top
Best
New

Posted by mpweiher 4/13/2025

Problems with Go channels (2016)(www.jtolio.com)
160 points | 197 commentspage 2
codr7 4/13/2025|
Agreed, channels are overrated and overused in Go.

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.

guilhas 4/13/2025||
Go channels, good or bad, are clearly a step up in concurrency/parallelism concepts and discourse

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

liendolucas 4/13/2025||
Putting aside this particular topic, I'm seeing posts talking negatively about the language. I got my feet wet with Go many many years ago and for unknown reasons I never kept digging on it, so...

Is it worth learning it? What problems are best solved with it?

jtolds 4/13/2025|
Author of the post here, I really like Go! It's my favorite language! It has absolutely nailed high concurrency programming in a way that other languages' solutions make me cringe to think through (await/async are so gross and unnecessary!)

If you are intending to do something that has multiple concurrent tasks ongoing at the same time, I would definitely reach for Go (and maybe be very careful or skip entirely using channels). I also would reach for Go if you intend to work with a large group of other software engineers. Go is rigid; when I first started programming I thought I wanted maximum flexibility, but Go brings uniformity to a group of engineers' output in a way that makes the overall team much more productive IMO.

Basically, I think Go is the best choice for server-side or backend programming, with an even stronger case when you're working with a team.

liendolucas 4/13/2025||
Thanks for the tip! Will definitely take into account your insights on channels if I decide to dive into it.
jtolds 4/13/2025||
I have written channel code in the last week. It's part of the deal (especially with the context package). I'm just happy to see them restrained.
fpoling 4/13/2025||
I once read a book from 1982 that was arguing about CSP implementation in Ada that it lead to proliferation of threads (called tasks in Ada) and code complexity when mutex -based solutions were simpler.

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.

sapiogram 4/13/2025|
Unfortunately, Go also made their channels worse by having their nil semantics be complete lunacy. See the "channel API is inconsistent and just cray-cray" section in the article.
chuckadams 4/13/2025||
The fact that Go requires a zero value for everything, including nil for objects like it was Java and the 1990's all over again, is one of the reasons I'm not particularly inclined to use Go for anything. Even PHP is non-nullable-by-default nowadays.
kbolino 4/13/2025|||
Nil is IMO far and away Go's biggest wart, especially today with a lot of the early language's other warts filed off. And there's really no easy way to fix this one.

I think this article on channels suffers from "Seinfeld is Unfunny" syndrome, because the complaints about channels have largely been received and agreed upon by experienced Go developers. Channels are still useful but they were way more prominent in the early days of Go as a solution to lots of problems, and nowadays are instead understood as a sharp tool only useful for specific problems. There's plenty of other tools in the toolbox, so it was easy to move on.

Whereas, nil is still a pain in the ass. Have any nontrivial data that needs to turn into or come from JSON (or SQL, or XML, or ...)? Chances are good you'll need pointers to represent optionality. Chaining structVal.Foo.Bar.Qux is a panic-ridden nightmare, while checking each layer results in a massive amount of boilerplate code that you have to write again and again. Heck, you might even need to handle nil slices specially because the other side considers "null" and "[]" meaningfully distinct! At least nil slices are safe in most places, while nil maps are panic-prone.

jtolds 4/13/2025||
> the complaints about channels have largely been received and agreed upon by experienced Go developers. Channels are still useful but they were way more prominent in the early days of Go as a solution to lots of problems, and nowadays are instead understood as a sharp tool only useful for specific problems.

As the author of the post, it's really gratifying to hear that this is your assessment nowadays. I agree, and while I'm not sure I had much to do with this turn of events (it probably would have happened with or without me), curbing the use of channels is precisely why I wrote the post. I felt like Go could be much better if everyone stopped messing around with them. So, hooray!

int_19h 4/13/2025||||
It's worse than that, though, because in Go you have many different kinds of nil once interfaces enter the picture.
NBJack 4/13/2025|||
Wait until you try refactoring in Go. Java has plenty of warts, but the explicit inheritance in it and other languages makes working across large codebases much simpler when you need to restructure something. Structural typing is surprisingly messy in practice.
chuckadams 4/13/2025||
I love structural typing, use it all day long in TypeScript, and wish every language had it. I've never run into issues with refactoring, though I suppose renaming a field in a type when the consumers are only matching by a literal shape might be less than 100% reliable. It looks like Go does support anonymous interfaces, but I'm not aware of how much they're actually used in real-world code (whereas in TS inlined object shapes are fairly common in trivial cases).
kbolino 4/13/2025|||
I too like it, in fact I find myself missing it in other languages. I will say that anonymous interfaces are kind of rare in Go, and more generally interfaces in my experience as used in real codebases seem to lean more on the side of being producer-defined rather than consumer-defined.

But there are a couple of problems I can think of.

The first is over-scoping: a structural interface is matched by everything that appears to implement it. This can complicate refactoring when you just want to focus on those types that implement a "specialized" form of the interface. So, the standard library's Stringer interface (with a single `String() string` method) is indistinguishable from my codebase's MyStringer interface with the exact same method. A type can't say it implements MyStringer but not Stringer. The solution for this is dummy methods (add another method `MyStringer()` that does nothing) and/or mangled names (change the only method to `MyString() string` instead of `String() string`) but you do have to plan ahead for this.

The second is under-matching: you might have intended to implement an interface only to realize too late that you didn't match its signature exactly. Now you may have a bunch of types that can't be found as implementations of your interface even though they were meant to be. If you had explicit interface implementation, this wouldn't be a problem, as the compiler would have complained early on. However, this too has a solution: you can statically assert an interface is supposed to be implemented with `var _ InterfaceType = ImplementingType{}` (with the right-hand side adjusted as needed to match the type in question).

NBJack 4/13/2025|||
I have to trace structure and data handoffs across multiple projects sometimes that span multiple teams. I'm used to rather easily being able to find all instances of something using a thing, where something is originally defined, etc. I can't do that easily in Go due to the structural typing at times without either a very powerful IDE (JetBrains is the only one I know of that does Go well via Goland) or intimate knowledge of the project. It's surprisingly painful, and our tooling while not Jetbrains level is suppose to be some of the best out there. And Yahweh help me if I actually have to refactor.

I really doubt this comes up in smaller, one-team repos. But in the coding jungle I often deal with, I spend much more time tracing code because of this issue than I'drefactoring. Make no mistake: I like Go, but this irks me.

ajankovic 4/13/2025||
I had time to spare so I toyed with the example exercise. Now I am not sure if I misunderstood something because solution is fairly simple using only channels: https://go.dev/play/p/tD8cWdKfkKW
jtolds 4/13/2025||
I replied to your comment on my website, but for posterity here, yes, I do think you did a good job for the part about exiting when bestScore > 100. There's nitpicks, but this is fine! It makes sense, and nice use of a select over a send.

I did expect that this exercise would come after the first one though, and doing this on top of a solution to the first exercise is a bit harder. That said, I also don't mean to claim either are impossible. It's just tough to reason about.

sateesh 4/13/2025||
Your example solution has only one player. Your solution won't work when there are multiple players.
ajankovic 4/13/2025||
I don't think you examined the code in full. main spawns 10 go routines that are constantly sending player scores to the game. That means 10 different players are sending their scores concurrently until someone reaches score of 100.
boruto 4/13/2025||
Quite a change in mood of comments compared to when it was posted last time. https://news.ycombinator.com/item?id=11210578
jtolds 4/13/2025|
Seriously! This caused such a ruckus when I posted this 9 years ago. I lost some professional acquaintanceships over it! Definitely a different reception.
om8 4/14/2025||
Channels are useful when they are really (rarely) needed. IMO Channel API should've been as ugly as reflect API to be considered only in extra cases.
gwd 4/13/2025||
He mentions the lack of generics; I wonder how he'd make his own channel++ library now that generics are well-established.
DeathArrow 4/13/2025||
I don't know Go, but can't the situation be improved somehow? Either make channels better or remove them altogether?
sapiogram 4/13/2025||
As a language user? Just stop using them entirely. It's what most people do eventually.
ReflectedImage 4/14/2025||
Well the problem is best stated as "I don't know how to use channels and I don't intend to learn". So perhaps some kinda of developer education is the solution?
dingdingdang 4/13/2025|
If channels are the wrong way to do things in Golang, what is the right way?
fpoling 4/13/2025|
In Go in many cases channels are unavoidable due to API. As already was pointed out in other threads a good rule of thumb is not to use them in public method signatures.

The valid use case for channels is to signal to consumer via channel close in Context.Done() style that something is ready that then can be fetched using separated API.

Then if you need to serialize access, just use locks.

WorkGroup can replace channels in surprisingly many cases.

A message passing queue with priorities implemented on top of mutexes/signals can be used in many cases that require complex interactions between many components.

dingdingdang 4/13/2025||
Cheers for clear summary, I take it you mean sync.WaitGroup and not WorkGroup? As a beginner I started up with WaitGroup usage years ago since it was more intuitive to me.. worked fine afaict.
fpoling 4/14/2025||
Yes, WaitGroup of cause.
pdimitar 4/15/2025||
I am here to lengthen your suffering: of course**
More comments...