"Go channels are bad and you should feel bad"

2,862 views
Skip to first unread message

andrey mirtchovski

unread,
Mar 2, 2016, 11:53:42 AM3/2/16
to golang-nuts
Subject is the title of the article, provided here without comment:

http://www.jtolds.com/writing/2016/03/go-channels-are-bad-and-you-should-feel-bad/

Nico

unread,
Mar 2, 2016, 1:29:52 PM3/2/16
to golan...@googlegroups.com
TL;DR;

* "Channels are slower than implementing it yourself" (i.e. channels are slower than mutexes)
* "it’s actually somewhat challenging to use channels alongside mutexes and condition variables correctly!"
* "Callbacks are strictly more powerful and don’t require unnecessary goroutines."
* "Almost every other operation in Go has a way to avoid a panic (type assertions have the , ok = pattern, for example), but with channels you just get to deal with it."
* "How could channels be better?"

I find interesting the idea of a "comma, ok" pattern for send statements. Is this something the Go authors have considered before?

Sam Whited

unread,
Mar 2, 2016, 1:52:10 PM3/2/16
to Nico, golang-nuts
On Wed, Mar 2, 2016 at 12:29 PM, Nico <nicolas...@gmail.com> wrote:
> I find interesting the idea of a "comma, ok" pattern for send statements. Is this something the Go authors have considered before?

This has been discussed here and on golang-dev.

TL;DR — Senders close channels, receivers check if they're closed. If
you're the sender and you send on a closed channel we panic because
you're doing it wrong; it's effectively an assertion that makes sure
you're using the pattern the way it was intended to be used.

—Sam



--
Sam Whited
pub 4096R/54083AE104EA7AD3
https://blog.samwhited.com

andrey mirtchovski

unread,
Mar 2, 2016, 1:56:28 PM3/2/16
to Sam Whited, Nico, golang-nuts
As a counterexample, the Go fork used for the Clive OS allows both
ends of a channel to close and propagates error messages on closing:

http://syssoftware.blogspot.com/2015/06/lsub-go-changes.html

Marvin Stenger

unread,
Mar 2, 2016, 1:56:47 PM3/2/16
to golang-nuts
One could lift the send statement to be an boolean expression, I guess.

Nico

unread,
Mar 2, 2016, 1:58:36 PM3/2/16
to Sam Whited, golang-nuts
On 02/03/16 18:51, Sam Whited wrote:
> TL;DR — Senders close channels, receivers check if they're closed. If
> you're the sender and you send on a closed channel we panic because
> you're doing it wrong; it's effectively an assertion that makes sure
> you're using the pattern the way it was intended to be used.

What when there are several senders?

An ok-pattern would help synchronise all the senders when the channel is closed by one of them.

Tapio Raevaara

unread,
Mar 2, 2016, 2:08:29 PM3/2/16
to golang-nuts
I have to admit that I've always wondered why send and receive operations on nil channel don't simply panic. Am I overlooking something obvious here? When is permanent blocking *ever* desirable behaviour in this case?

Egon

unread,
Mar 2, 2016, 2:08:41 PM3/2/16
to golang-nuts, s...@samwhited.com
On Wednesday, 2 March 2016 20:58:36 UTC+2, Nico wrote:
On 02/03/16 18:51, Sam Whited wrote:
> TL;DR — Senders close channels, receivers check if they're closed. If
> you're the sender and you send on a closed channel we panic because
> you're doing it wrong; it's effectively an assertion that makes sure
> you're using the pattern the way it was intended to be used.

What when there are several senders?

Last one sender to leave, turns off the lights, which can be controlled by a atomic int.

Nathan Fisher

unread,
Mar 2, 2016, 2:22:47 PM3/2/16
to Nico, Sam Whited, golang-nuts
Out of curiosity what scenario would you see it being good to have multiple publishers to shutdown a channel?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Nico

unread,
Mar 2, 2016, 2:22:52 PM3/2/16
to golan...@googlegroups.com
On 02/03/16 19:08, Egon wrote:
> Last one sender to leave, turns off the lights, which can be controlled by a atomic int.

A.K.A. sync.WaitGroup.

The ok-pattern would make sync.WaitGroup unnecessary in these cases.

Nico

unread,
Mar 2, 2016, 2:23:39 PM3/2/16
to golan...@googlegroups.com
On 02/03/16 18:58, Tapio Raevaara wrote:
> I have to admit that I've always wondered why send and receive operations on nil channel don't simply panic. Am I overlooking something obvious here? When is permanent blocking *ever* desirable behaviour in this case?

It's useful within a select.

Tapio Raevaara

unread,
Mar 2, 2016, 3:16:51 PM3/2/16
to golang-nuts

I don't understand. Doesn't select essentially ignore cases that operate on nil channels anyway? I'm certainly not suggesting that select should panic if one of the channels is nil.

Sam Whited

unread,
Mar 2, 2016, 3:19:43 PM3/2/16
to Tapio Raevaara, golang-nuts
I'm assuming that by "nil" channel you mean one that has nothing in it?

There are lots of use cases for this; in fact, it's an important part
of using channels. For example, you might use this blocking to create
a simple semaphore using channels:

sem := make(chan int, MaxJobs)

func SomeJobHandler(queue chan *Job) {
for job := range queue {
job := job // Copy and shadow request since Go reuses vars
declared in the for statement
sem <- 1// Will block after the buffer is full, eg. when
MaxJobs are running
go func() {
process(job) // Some expensive processing
<-sem
}()
}
}

Of course, this can be handled in other ways, but this is just an example.
Another common example can be found in the time package (the first
case won't be hit until the timeout time has passed):

select {
case <-After(timeout): // This is similar to calling sleep, except
that we can use it in a select statement where we definitely don't
want the read to panic!
return // We've timed out
default:
// Do some work
}

These are just a few simple examples of why we might want to block on
a channel read.

Tapio Raevaara

unread,
Mar 2, 2016, 3:29:22 PM3/2/16
to golang-nuts, tapio.r...@iki.fi


On Wednesday, March 2, 2016 at 10:19:43 PM UTC+2, Sam Whited wrote:
On Wed, Mar 2, 2016 at 12:58 PM, Tapio Raevaara <tapio.r...@iki.fi> wrote:
> I have to admit that I've always wondered why send and receive operations on
> nil channel don't simply panic. Am I overlooking something obvious here?
> When is permanent blocking *ever* desirable behaviour in this case?

I'm assuming that by "nil" channel you mean one that has nothing in it?

No, I mean (chan T(nil)), i.e. a channel that hasn't been initialized. Encountering one is almost certainly a bug, which is why I don't understand why operating on one just silently locks the goroutine instead of panicking. Like I wrote earlier, I don't see how blocking  could ever be desired behaviour in this case.

Sam Whited

unread,
Mar 2, 2016, 3:32:43 PM3/2/16
to Tapio Raevaara, golang-nuts
On Wed, Mar 2, 2016 at 2:18 PM, Sam Whited <s...@samwhited.com> wrote:
> I'm assuming that by "nil" channel you mean one that has nothing in it?

Nevermind, just realized that you meant eg.

var c chan int;

this is different, but still useful.

Eg. imagine you do a select on a nil channel:

func dowork(c chan int) {
select {
case <-c
// Do some work if not nil
default:
return // Don't do anything:
}
}

You don't have to do a nil check on this function, the correct thing
just happens.

—Sam

Nico

unread,
Mar 2, 2016, 3:34:30 PM3/2/16
to golan...@googlegroups.com

Sam Whited

unread,
Mar 2, 2016, 3:38:29 PM3/2/16
to Nico, golang-nuts
On Wed, Mar 2, 2016 at 2:34 PM, Nico <nicolas...@gmail.com> wrote:
> https://talks.golang.org/2013/advconc.slide#29

Exactly, "select never selects a blocking case" is what we're taking
advantage of in the example I sent. If you DO pass in a non-nil
channel, it will be hit, otherwise (if you pass in nil) the default
case will be hit.

—Sam

Tapio Raevaara

unread,
Mar 2, 2016, 4:06:00 PM3/2/16
to golang-nuts, tapio.r...@iki.fi

Why are bringing up select, I'm not suggesting that its semantics should be changed in any way. Select chooses a case where *communication can proceed*, so if an operation on nil channel would panic, select would still ignore those cases, just like it does right now. The following cases are equivalent:

select {
case <-ch:
    break
}

select {
case <-ch:
    break
case <-(chan int(nil)):
}

These would still remain equivalent even if a normal send and receive on a nil channel would panic, because communication on a nil channel cannot proceed.
 
Again, how is it *ever desirable behaviour* that a simple send or receive on a nil channel will just block forever, with no way to recover? Am I missing something here?

Tapio Raevaara

unread,
Mar 2, 2016, 4:14:10 PM3/2/16
to golang-nuts, tapio.r...@iki.fi


On Wednesday, March 2, 2016 at 11:06:00 PM UTC+2, Tapio Raevaara wrote:
select {
case <-ch:
    break
}

select {
case <-ch:
    break
case <-(chan int(nil)):
}

Sorry, that example doesn't make any sense, I'm not sure what strange fumes I was inhaling...

The point is that a case with an operation on a nil channel will never be selected, and that wouldn't change if nil channel operations would panic outside of select. Which is why I don't see how blocking *instead of panicking* on nil channel operation is in any way "useful".

mikespook

unread,
Mar 2, 2016, 4:23:13 PM3/2/16
to Sam Whited, Tapio Raevaara, golang-nuts
If you passed a nil chan into the function `dowork`, as far as I can concern:

Yes, the behavior is correct. 
But,
No, the logic is wrong.

The implicit meaning of `case <-c` would cause a big trouble in the feature.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
Xing Xing (邢星)
mikespook <mike...@gmail.com>
http://mikespook.com


Sam Whited

unread,
Mar 2, 2016, 4:25:21 PM3/2/16
to Tapio Raevaara, golang-nuts
On Wed, Mar 2, 2016 at 3:14 PM, Tapio Raevaara <tapio.r...@iki.fi> wrote:
> The point is that a case with an operation on a nil channel will never be
> selected, and that wouldn't change if nil channel operations would panic
> outside of select. Which is why I don't see how blocking *instead of
> panicking* on nil channel operation is in any way "useful".

That's not how panic works; panic is for things that should _never_
happen (eg. it's like an assert in other languages). It's a way to
fail fast if something catastrophically bad happens (eg. code that's
impossible to hit is hit because someone screwed something up, a
critical error that could lead to a security compromise happened,
etc.). If something were to panic in a select case, we wouldn't want
it to magically recover the panic and not select that case, we'd still
want to know about it.

You're right, the language could have used panic differently and made
reads from nil channels panic and had select catch them, but it
wouldn't be the same language. This was a deliberate choice to make
things like the example I made possible without magically covering up
serious errors.

TL;DR — I think you're thinking that panic is used for something it
wasn't designed for; blocking is much cleaner.

—Sam

Nico

unread,
Mar 2, 2016, 4:28:38 PM3/2/16
to golan...@googlegroups.com
On 02/03/16 21:06, Tapio Raevaara wrote:
> Why are bringing up select?

If send and receive on nil channel didn't block, then the program in https://talks.golang.org/2013/advconc.slide#29 wouldn't work.

> Select chooses a case where *communication can proceed*, so if an operation on nil channel would panic, select would still ignore those cases, just like it does right now.

Just to be clear, a send statement on a closed channel within a select also panics. See:

https://play.golang.org/p/FUbHw_ey8I

package main

func main() {
ch := make(chan struct{})
close(ch)
select {
case ch <- struct{}{}:
default:
}
}


Alex Bligh

unread,
Mar 2, 2016, 4:35:40 PM3/2/16
to Nico, Alex Bligh, golan...@googlegroups.com

On 2 Mar 2016, at 21:28, Nico <nicolas...@gmail.com> wrote:

> Just to be clear, a send statement on a closed channel within a select also panics. See:
>
> https://play.golang.org/p/FUbHw_ey8I
>
> package main
>
> func main() {
> ch := make(chan struct{})
> close(ch)
> select {
> case ch <- struct{}{}:
> default:
> }
> }

func main() {
ch := make(chan struct{})
close(ch)
select {
case ch, ok <- struct{}{}:
default:
}
}

The above looks horrible I grant you, but making that return false in ok rather than
panicing would not (I think) break anything. Currently it gives 'select cases cannot be lists'
obviously.

--
Alex Bligh




Tapio Raevaara

unread,
Mar 2, 2016, 5:00:13 PM3/2/16
to golang-nuts

 Hm... I looked at the spec and I'm wondering *why* that panics. Sending on a closed channel is supposed to panic, but select is only supposed to pick a case where *communication can proceed* (or the default case, of course). Am I misreading the spec somehow?

Oh, and thanks for linking to that program a second time, I totally misread that the first time. Indeed, that program wouldn't work if the semantics were what I'm suggesting, so in a sense the current semantics can be considered "useful". Still, the exact same behaviour would be achieved by wrapping the sends in a select. I maintain that in vast majority of cases trying to operate on a nil channel is *bug* and runtime should panic - like it does if you try to close a nil channel. The fact is that every time you send/receive on a nil channel (without select), you *silently* leak a goroutine.

Dan Kortschak

unread,
Mar 2, 2016, 5:28:35 PM3/2/16
to Tapio Raevaara, golang-nuts
On Wed, 2016-03-02 at 12:29 -0800, Tapio Raevaara wrote:
> No, I mean (chan T(nil)), i.e. a channel that hasn't been
> initialized.
> Encountering one is almost certainly a bug, which is why I don't
> understand why operating on one just silently locks the goroutine
> instead of panicking. Like I wrote earlier, I don't see how blocking
> could ever be desired behaviour in this case.

It can be used to conditionally block a select branch. This is very
useful behaviour.

Dave Cheney

unread,
Mar 2, 2016, 5:28:37 PM3/2/16
to golang-nuts
Have a look back in the git history to the point when send and receive used to work like this. There is lots of historical discussion, pre go 1.0, that explains why things are the way they are.

Nico

unread,
Mar 2, 2016, 5:32:00 PM3/2/16
to Nathan Fisher, Sam Whited, golang-nuts
On 02/03/16 19:22, Nathan Fisher wrote:
> Out of curiosity what scenario would you see it being good to have multiple publishers to shutdown a channel?

Just out of curiosity:

* here's a fan-in pattern that leaks goroutines: https://play.golang.org/p/oYtXpuOoMJ

* and here, using the currently-invalid "comma, ok" pattern on send statements: https://play.golang.org/p/3MC4JSdJ8t

package main

import "fmt"

func main() {
ch := fanIn(seq(5), seq(10))

for i := range ch {
fmt.Println(i)
if i > 5 {
break
}
}

close(ch) // invalid operation: close(ch) (cannot close receive-only channel)
}

func seq(n int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < n; i++ {
// "comma, ok" pattern on send statements
if ch, ok <- i; !ok {
break
}
}
}()
return ch
}

func fanIn(inputs ...<-chan int) <-chan int {
ch := make(chan int)

for _, i := range inputs {
i := i
go func() {
for n := range i {
// "comma, ok" pattern on send statements
if ch, ok <- n; !ok {
close(i) // invalid operation: close(i) (cannot close receive-only channel)
break
}
}
}()
}

return ch
}

Marvin Stenger

unread,
Mar 2, 2016, 5:42:26 PM3/2/16
to golang-nuts, nfi...@junctionbox.ca, s...@samwhited.com
"comma, ok" pattern on send statements is no option, looks way too odd and ugly.
Making the send statement a boolean expression would be the cleaner way to implement this idea

Tapio Raevaara

unread,
Mar 2, 2016, 5:42:41 PM3/2/16
to golang-nuts
 I'm talking specifically about send/receive *outside* of select. I though it was obvious that select would ignore those cases anyway, but I guess I should've made that even more obvious. :)


On Thursday, March 3, 2016 at 12:28:37 AM UTC+2, Dave Cheney wrote:
Have a look back in the git history to the point when send and receive used to work like this. There is lots of historical discussion, pre go 1.0, that explains why things are the way they are.

 Communication on nil channel *used* to panic? I've got to admit I've totally forgotten this...! Thanks for pointing this out.

Hm, need to do some digging... I'd still greatly appreciate any links to discussions or examples of the usefulness of the current behaviour.

Nico

unread,
Mar 2, 2016, 5:46:15 PM3/2/16
to golan...@googlegroups.com
On 02/03/16 22:42, Marvin Stenger wrote:
> "comma, ok" pattern on send statements is no option, looks way too odd and ugly.
> Making the send statement a boolean expression would be the cleaner way to implement this idea

Yes, I also like it better as a boolean expression:

https://play.golang.org/p/cfRv67pEzJ

package main

import "fmt"

func main() {
ch := fanIn(seq(5), seq(10))

for i := range ch {
fmt.Println(i)
if i > 5 {
break
}
}

close(ch) // invalid operation: close(ch) (cannot close receive-only channel)
}

func seq(n int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < n; i++ {
// "comma, ok" pattern on send statements
if ok := ch <- i; !ok {
break
}
}
}()
return ch
}

func fanIn(inputs ...<-chan int) <-chan int {
ch := make(chan int)

for _, i := range inputs {
i := i
go func() {
for n := range i {
// "comma, ok" pattern on send statements
if ok := ch <- n; !ok {

Nigel Tao

unread,
Mar 2, 2016, 6:10:09 PM3/2/16
to Tapio Raevaara, golang-nuts
On Thu, Mar 3, 2016 at 9:42 AM, Tapio Raevaara <tapio.r...@iki.fi> wrote:
> On Thursday, March 3, 2016 at 12:28:37 AM UTC+2, Dave Cheney wrote:
>>
>> Have a look back in the git history to the point when send and receive
>> used to work like this. There is lots of historical discussion, pre go 1.0,
>> that explains why things are the way they are.
>
>
> Communication on nil channel *used* to panic? I've got to admit I've
> totally forgotten this...! Thanks for pointing this out.
>
> Hm, need to do some digging... I'd still greatly appreciate any links to
> discussions or examples of the usefulness of the current behaviour.

It's not so much that it's useful. It's that it's consistent (see e.g.
https://groups.google.com/forum/#!msg/golang-dev/vO9XVbNkC-Q/Hr5qBl7KwgQJ
and https://groups.google.com/forum/#!msg/golang-nuts/WrF9kBDhGEg/pLRYVMByMf0J).

https://groups.google.com/forum/#!msg/golang-dev/VK_L5x1EgBI/0y43UG94qqIJ
says that:

----
For example, given that a select
is defined to wait for one of a collection of cases to be ready
and then execute it, a single-case select:

select {
case xxx:
yyy
}

should, since it is waiting on only a single communication, behave
the same as running the case directly:

{
xxx
yyy
}

Losing that property would, in my opinion, be a serious mistake.
It would make communications behave differently inside select
and outside select, introducing special cases for programmers
to stumble across.

It would be like

switch x {
case y:
...
}

being different from

if x == y {
...
}
----

Dave Cheney

unread,
Mar 2, 2016, 6:17:47 PM3/2/16
to golang-nuts
Former: The language change happened in r57, back in 2011, https://golang.org/doc/devel/pre_go1.html#r57. The change is spread across multiple commits as it was done in two stages, https://codereview.appspot.com/3973051, incidentally this was the introduction and first use of gofix.

Nico

unread,
Mar 2, 2016, 6:42:35 PM3/2/16
to golan...@googlegroups.com
On 02/03/16 23:17, Dave Cheney wrote:
> Former: The language change happened in r57, back in 2011, https://golang.org/doc/devel/pre_go1.html#r57. The change is spread across multiple commits as it was done in two stages, https://codereview.appspot.com/3973051, incidentally this was the introduction and first use of gofix.
>
> Latter: http://dave.cheney.net/2014/03/19/channel-axioms

Dave, thanks for digging this out!

Christopher Brodt

unread,
Mar 2, 2016, 6:49:59 PM3/2/16
to golan...@googlegroups.com
It seems like lock contention is a major pain point with channels in Go. What would some potential solutions be? I don't suppose a lock-free implementation is feasible.

Tapio Raevaara

unread,
Mar 2, 2016, 6:56:35 PM3/2/16
to golang-nuts, tapio.r...@iki.fi

I can understand that point of view. Then again, according to the spec, those cases aren't exactly the *same* thing, even if they *behave* the same way right now. In the first case it's the *select* that blocks because no communication can happen, in the second one it's the communication operation itself that blocks... but I guess that difference might be a bit too subtle for some. :)

Nevertheless, I find it really ugly that nil channel ops simply block silently and permanently. Other blocking operations can be recovered from. Panics can be recovered from. Read from a nil channel... that goroutine is forever gone.
 

Dave Cheney

unread,
Mar 2, 2016, 7:04:13 PM3/2/16
to golang-nuts


On Thursday, 3 March 2016 10:49:59 UTC+11, Christopher Brodt wrote:
It seems like lock contention is a major pain point with channels in Go. What would some potential solutions be? I don't suppose a lock-free implementation is feasible.

This is not true. I encourage you to check out the benchmarks in the runtime package.

ch...@uberbrodt.net

unread,
Mar 2, 2016, 7:18:13 PM3/2/16
to golang-nuts
Dave, I will take a look at those benchmarks, thanks. I've been led to believe that there's a significant cost to using channels vs mutexes by the OP's article and the following:

http://jmoiron.net/blog/go-performance-tales/
http://bravenewgeek.com/go-is-unapologetically-flawed-heres-why-we-use-it/

Personally, I've not had an issue, though I'm a relatively recent Go programmer.

Dave Cheney

unread,
Mar 2, 2016, 7:47:04 PM3/2/16
to golang-nuts
Sending data to another goroutine will never be free, there is always some overhead. You should use channels to communicate meaningful amounts of work and consider if two operations are so tightly coupled that one cannot proceed without the other, is your problem actually sequential, rather than concurrent.

tsuna

unread,
Mar 2, 2016, 8:00:47 PM3/2/16