Most active commenters
  • sagichmal(5)
  • mordocai(4)
  • pmarreck(4)
  • zeeboo(3)

←back to thread

Go channels are bad

(www.jtolds.com)
298 points jtolds | 31 comments | | HN request time: 0.701s | source | bottom
1. sagichmal ◴[] No.11211112[source]
This is a frustrating and overly-exasperated post which reaches conclusions that have always been part of the Go canon. APIs should be designed synchronously, and the callers should orchestrate concurrency if they choose -- yes! Channels are useful in some circumstances, but if you just want to synchronize access to shared memory (like the author's example) then you should just use a mutex -- yes! These are well understood truths.

Novices to the language have a tendency to overuse channels. Here is Andrew Gerrand addressing precisely this point two years ago at GopherCon: https://www.youtube.com/watch?v=u-kkf76TDHE&t=815

Neither the referenced performance characteristics via Tyler Treat, nor the FUD surrounding channel-based program architecture, invalidate channels generally. One does have to think carefully about ownership hierarchies: only one goroutine gets to close the channel. And if it's in a hot loop, a channel will always perform worse than a mutex: channels use mutexes internally. But plenty of problems are solved very elegantly with channel-based CSP-style message passing.

It's unfortunate that articles like this are written and gain traction. The explicit instruction to [new] Go programmers is that they should avoid channels, even that they are badly implemented, and both of those things are false.

replies(7): >>11211239 #>>11211262 #>>11211272 #>>11211656 #>>11211660 #>>11214091 #>>11228064 #
2. mordocai ◴[] No.11211239[source]
As a non-go programmer, I'm pretty sure the author made some very good objective arguments that channels are in fact badly implemented.
replies(2): >>11211298 #>>11211519 #
3. bad_user ◴[] No.11211262[source]
> APIs should be designed synchronously, and the callers should orchestrate concurrency if they choose

Wait, why would you say that?

In general, if "orchestrating concurrency" involves guarding access to shared mutable state, then you can't orchestrate it at the callers site. It would be a massive encapsulation leak, because synchronization is not composable, requires special knowledge, plus you don't necessarily know how to synchronize unless you have knowledge about internals. Furthermore, because it is after the fact, your only choice of handling it is by means of mutexes, which has really terrible performance characteristics. Even if you could do ordering by other means, you end up paying the price of LOCK XCHG or whatever mutexes translate to, not to mention that you'll have problems if you want (soft) real-time behavior, because now you can end up with both dead-locks and live-locks.

And this brings us to another problem. If you end up doing such synchronization in Go, then Go's M:N multi-threading ends up doing more harm than good, because if you need such synchronization, you also need to fine tune your thread-pools and at this point 1:1 would be better. On top of 1:1 platforms you can build M:N solutions, but it doesn't work well in the other direction.

> Novices to the language have a tendency to overuse channels

Novices to software development have a tendency to overuse mutexes as well.

replies(4): >>11211326 #>>11211353 #>>11211663 #>>11222537 #
4. mordocai ◴[] No.11211272[source]
In addition, the author points out existing go libraries that people use that use channels when they shouldn't, so apparently the go language community needs more people pointing out that this is a bad idea.

(I decided to make a new comment rather than edit my existing comment)

5. sagichmal ◴[] No.11211298[source]
Read Tyler's original article for a less FUDdy take on it. Channels are always slower than mutexes, which is obvious when you understand their implementation. They are definitely not badly implemented as a general rule.
replies(1): >>11212165 #
6. sagichmal ◴[] No.11211326[source]
> In general, if "orchestrating concurrency" involves guarding access to shared mutable state, then you can't orchestrate it at the callers site.

Shared mutable state is generally behind an API boundary. I'm talking about the exported method set of that API. That is,

    func (f *Foo) Update(i int) (int, error)             // yes
    func (f *Foo) Update(i int) (<-chan int, error)      // no
    func (f *Foo) Update(i int, result chan<- int) error // no
7. jerf ◴[] No.11211353[source]
"In general, if "orchestrating concurrency" involves guarding access to shared mutable state, then you can't orchestrate it at the callers site."

What this generally means in Go is that it is an antipattern for your library to provide something like "a method that makes an HTTP request in a goroutine". In Go, you should simply provide code that "makes an HTTP request", and it's up to the user to decide whether they want to run that in a goroutine.

The rest of what you're talking about is a completely different issue.

Channels are smelly in an API. IIRC in the entire standard library there's less than 10 functions/methods that return a channel. But the use case does occasionally arise.

8. voidlogic ◴[] No.11211519[source]
Hammers make poor screw drivers. Mutexes, atomic integer operations, and channels (buffered and unbuffered) all have their place. You will think any of these is "badly implemented" if you choose the wrong tool for the job.
9. admiun ◴[] No.11211656[source]
"APIs should be designed synchronously, and the callers should orchestrate concurrency if they choose"

Just to add to this, I found the blog post he mentions[1] towards the bottom that supports this conclusion a really good read.

[1] http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...

10. jtolds ◴[] No.11211660[source]
This post was written primarily as a response to http://www.informit.com/articles/article.aspx?p=2359758, which, when it came out last June, frustrated me to no end. It then sat in my drafts folder for months until patiently attempting to bring another experienced programmer, new-to-Go, up to speed on best practices.

If it truly is the accepted best practice for novices to avoid channels, then that PR campaign has been tried and found lacking. EDIT: whoops, read parent wrong.

replies(1): >>11211698 #
11. pmarreck ◴[] No.11211663[source]
Reading this... only makes me gladder that I'm pursuing work in the Erlang/Elixir space, where messaging "just works" and concurrency "just works" and immutability "just works" (and new processes take a microsecond to spin up) and tearing anything down is basically a nonissue as failure is embraced and logged at every turn and cascading teardowns simply happen automatically depending on how the processes are linked

and all this turns out to be a really amazing system of paradigms when designing apps to work in the real world

replies(1): >>11212034 #
12. d_theorist ◴[] No.11211698[source]
I think you have the sense of the last sentence of the parent comment backwards.
replies(1): >>11211717 #
13. jtolds ◴[] No.11211717{3}[source]
Oh, so I did.
14. svanderbleek ◴[] No.11212034{3}[source]
It "just works" in Go too, minus immutability, but congrats on your technology decision. You don't get type checking but c'est la vie.
replies(3): >>11212486 #>>11213900 #>>11214370 #
15. zeeboo ◴[] No.11212165{3}[source]
The api definitely is badly implemented and makes them hard to use. That's the point of the post. There are design decisions around channels (sends panicing, close panicing, nil channels blocking) that make them hard to understand, follow, and compose concurrent solutions.
replies(1): >>11212804 #
16. raould42 ◴[] No.11212486{4}[source]
Dialyzer
17. sagichmal ◴[] No.11212804{4}[source]
I'm sorry, but I don't agree with any of your assertions. The constraints on channels are there not as an accident of a bad implementation, but as deliberate decisions to enforce a certain set of design contracts. Panics on invalid channel operations enforce those contracts. That nil channels block is actually an incredibly handy feature: see e.g. https://github.com/streadway/handy/blob/b8cb168/breaker/brea...

Without exception, hitting one of these corner cases exposes an error in design, from Go's perspective on CSP. You can disagree with that perspective on a subjective basis ("hard to understand") -- but you can't lift that opinion to objective fact, and you certainly can't claim these artifacts of design as evidence of incompetence or neglect.

replies(2): >>11213487 #>>11213762 #
18. mordocai ◴[] No.11213487{5}[source]
While I agree all of this is subjective, I would argue that something being composed of "deliberate decisions to enforce a certain set of design contracts" doesn't mean those decisions nor the design contracts are good. Nor does it automagically make a good implementation.

In addition, making bad design decisions that you think are good is actually one of the best types of evidence for incompetence (though not neglect, in this case).

I don't personally have enough data to have a strong opinion on where Go channels falls here, but I don't think any of your arguments here have any bearing on the idea that Go's channel implementation is bad.

replies(1): >>11213567 #
19. AnimalMuppet ◴[] No.11213567{6}[source]
But using something in the way it was not intended to be used, and then complaining that it works badly, is evidence of incompetence on the part of the user, not the designer.

> I don't personally have enough data to have a strong opinion on where Go channels falls here, but I don't think any of your arguments here have any bearing on the idea that Go's channel implementation is bad.

If sagichmal is correct, zeeboo is trying to use channels in a way that they were explicitly not designed to be used. That makes zeeboo's criticism very likely to be invalid. (It is the one who uses them as they were designed to be used who knows what the actual problems with the design are.)

replies(2): >>11213634 #>>11213771 #
20. mordocai ◴[] No.11213634{7}[source]
That's where the purely subjective argument comes in I suppose.

The argument could be made that go's channels SHOULD be able to handle zeeboo's use case and the fact that they weren't designed to be able to handle it makes them bad.

replies(1): >>11213875 #
21. zeeboo ◴[] No.11213762{5}[source]
It's great that panics happen when you violate those contracts. That is a deliberate design decision and I agree with it. However, the contracts that they enforce cause real problems evidenced by the article. Small additions might make those contracts more general and make channels more applicable. In my opinion, you should be able to attempt to send on a channel that could be closed in the same way that you are allowed to check if an interface contains a specific concrete type without panicing. In my experience, this would allow for a number of useful patterns that are very hard to express right now.

Nil channels blocking is definitely a deliberate design decision and has valid use cases. I use them frequently when I have a channel based design. It also isn't the thing that most people first expect since they have the opposite analog for using anything else that is nil: panics. The article which I assume you read makes only this point.

I never attempted to lift statements that are obviously opinion based (anything that has a judgement of something good or bad) as objective fact.

Here's a proposal I worked on with a coworker to make channels better that might give you more of an idea of why I'm suggesting that the current design has flaws: https://github.com/golang/go/issues/14601

Given how much weight channels are in the language specification and memory model, it would be nice if they were more generally applicable and easier to use for more concurrency situations.

22. zeeboo ◴[] No.11213771{7}[source]
My criticism is that the design limits the places where they are valid. I'm not trying to use a hammer where a screwdriver is required, I'm saying that if the hammer was designed differently, we'd be able to use it in more situations appropriately.

It's as if someone created a gun that fired backwards and I said "hey, it might be better if the gun fired forwards. we'd be able to use it in more situations." and people responded with "you shouldn't use a gun that fires backwards if you want to fire forwards." I totally agree, but it's missing the point.

23. AnimalMuppet ◴[] No.11213875{8}[source]
Only if go doesn't have a good way of handling that use case (even if it is something completely different from channels). I don't know enough to know whether it does.
24. pmarreck ◴[] No.11213900{4}[source]
Restricting input based on type hierarchies can reduce a certain class of bugs, yes, but careful use of guards as well as typespecs and unit test coverage (which you should have, anyway) can accomplish much of what type restrictions can
replies(1): >>11214838 #
25. ◴[] No.11214091[source]
26. im_down_w_otp ◴[] No.11214370{4}[source]
Save the following code in "someone_was_wrong_on_the_internet.erl" and then run "dialyzer --src someone_was_wrong_on_the_internet.erl"

  -module(someone_was_wrong_on_the_internet).
  -export([init/0, fizzbuzzer/1]).
  
  -spec init() -> list(pos_integer() | binary()).
  init() ->
      List = [-1, 0, 0.1, 1, 2, 3, 5, 15, 2, 3, 5, 15, 1],
      [fizzbuzzer(Result) || Result <- List].
  
  -spec fizzbuzzer(pos_integer()) -> pos_integer() | binary().
  fizzbuzzer(Number) when Number rem 15 =:= 0 ->
      <<"FizzBuzz">>;
  fizzbuzzer(Number) when Number rem 5 =:= 0 ->
      <<"Buzz">>;
  fizzbuzzer(Number) when Number rem 3 =:= 0 ->
      <<"Fizz">>;
  fizzbuzzer(Number) ->
      Number.

Dialyzer will fail the type check until you remove [-1, 0, 0.1] from the list. Not with a particularly helpful error, but it does fail it nonetheless.

The code itself is a valid program that runs, but it produces incorrect output, because 0 rem 15 =:= 0, so you get <<"FizzBuzz">> where you'd expect to get a 0 in the list. By running Dialyzer in my build chain I can catch that my implementation doesn't match my constraints at compile-time. In a way that I otherwise would have only found at runtime.

Though while creating this little pointless example one thing I'm not super clear on is why Dialyzer fails to notice that my return type from

  fizzbuzzer(Number) ->
      Number.
if I change it to

  fizzbuzzer(Number) ->
      -Number.
will return a neg_integer() and fail to satisfy the return spec. Despite that I've told it the input must be a be a pos_integer(). Unless I enable the -Wspecdiffs flag, in which case it notices the problem.
27. pmarreck ◴[] No.11214838{5}[source]
Was something I said factually wrong? User im_down_w_otp put up an example of what I'm talking about (minus the unit testing) so what gives?
replies(1): >>11216739 #
28. sagichmal ◴[] No.11216739{6}[source]
I suspect dismissing static typing whole cloth with "unit tests and certain guards can give you most of the benefits" comes off badly to some people.
replies(1): >>11225202 #
29. pron ◴[] No.11222537[source]
> you end up paying the price of LOCK XCHG or whatever mutexes translate to

But channels use locks internally. The choice of channels vs. mutexes is one of design, not implementation. Also, mutexes are blocking; LOCK XCHG isn't. Sure, mutexes also use LOCK XCHG (but so do channels, and nearly all concurrent data sctructures), but they also block (as do channels).

> your only choice of handling it is by means of mutexes, which has really terrible performance characteristics

That's just not true. There is a way to translate any locking algorithm to a non-blocing one (in fact, wait-free, which is the "strongest" non-blocking guarantee), yet only only a handful of wait-free algorithms are used in practice. Why? Because it's hard to make them more efficient than locks in the general case.

> not to mention that you'll have problems if you want (soft) real-time behavior, because now you can end up with both dead-locks and live-locks.

Again, channels are blocking data structures.

> If you end up doing such synchronization in Go, then Go's M:N multi-threading ends up doing more harm than good, because if you need such synchronization, you also need to fine tune your thread-pools and at this point 1:1 would be better.

I don't know where you're getting that. AFAIK, Go's mutexes don't block the underlying thread; only the goroutine.

The question of which concurrency mechanism should be used is a difficult one (and in general, more than one is necessary; even Erlang has shared, synchronized mutable state with its ETS tables), but you are very misinformed about how concurrency constructs are built and about their performance behavior.

30. pmarreck ◴[] No.11225202{7}[source]
I don't see how "can accomplish much of what type restrictions can" is the equivalent of "dismissing static typing whole cloth"

I choose wording carefully for a reason

31. nfirvine ◴[] No.11228064[source]
"Novices to the language have a tendency to overuse channels." "The explicit instruction to [new] Go programmers is that they should avoid channels... [is] false." -- Isn't this a contradiction?