
Go channels are bad - jtolds
http://www.jtolds.com/writing/2016/03/go-channels-are-bad-and-you-should-feel-bad/
======
sagichmal
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](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.

~~~
bad_user
> _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.

~~~
pmarreck
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

~~~
svanderbleek
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.

~~~
pmarreck
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

~~~
pmarreck
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?

~~~
sagichmal
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.

~~~
pmarreck
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

------
david-given
I've always really, really liked Ada's rendezvous-based concurrency.

There's more to it than I can really describe here, but in effect it allows
you to treat a thread as an object with methods; calling a method on the
object sends a message to the thread. The thread's main code can, at any
point, block and wait for a message, or combination of messages.

The handling code looks like this:

    
    
        ...some code...
        accept DoSomething(value: in out integer) do
          ...some code here...
        end
        ...some more code...
    

That causes the thread to block and wait for the message. When the message is
sent, the caller blocks, the receiver runs the handler, then the caller
resumes.

The beauty here is that inside the message handler, you know that the caller
is blocked... which means it's safe to pass parameters by pointer[ _].
Everywhere the parameter 's in scope, the parameter is safe to use. The type
system won't let the thread store the pointer anywhere without copying the
contents first, so you get zero-copy messaging _and* it's failsafe.

You can also do really cool stuff with timeouts, guards, automatic thread
termination, etc. Here's a simple multithreaded queue (syntax and logic not
guaranteed, it's been a while):

    
    
        loop
          select
            when usage < capacity =>
              accept Push(value: in integer) do
                data[usage] := value;
                usage := usage + 1;
              end;
          or
            when usage > 0 =>
              accept Pop(value: out integer) do
                usage := usage - 1;
                value := data[usage];
              end;
          or
            terminate;
          end select;
        end loop;
    

Multithreaded! Blocks the client automatically if they pop while the queue's
empty or push while it's full! Automatically terminates the thread when the
last connection goes away and the thread leaves scope! Thread safe! Readable!

I'd love to be able to do this in a more mainstream language.

[*] This is a simplification. Ada's pointers are not like other language's
pointers.

~~~
msbarnett
The older I get, the clearer it is that Ada was the answer to the last 2
decades worth of problems (fast, able to go low-level when you need to, very
type safe, easy concurrency primitives) and we all just ignored it because it
wasn't fashionable.

~~~
david-given
I would _love_ to have a modernised Ada. With case sensitivity. And garbage
collection (a lot of the language semantics are obviously intended to be based
around having a garbage collector. I'm very surprised that it never seemed to
get one). And a less wacky OO system (invisible syntax, ugh).

But those are quibbles, and at it's heart it's still an excellent, excellent
language. And there are Ada compilers in Debian, it's still being maintained,
it compiles really quickly into excellent code, it interoperates beautifully
with C...

~~~
msbarnett
I don't mind the lack of GC. Storage pools are reminiscent of Objective-C's
autorelease pools, which I've always thought were a very nice way of handling
a group of objects' lifetimes.

> And a less wacky OO system (invisible syntax, ugh).

Not sure what you mean by this one?

> And there are Ada compilers in Debian, it's still being maintained, it
> compiles really quickly into excellent code, it interoperates beautifully
> with C...

I came across [http://www.getadanow.com](http://www.getadanow.com) the other
day. Really easy way to get Ada going on OS X, too.

~~~
david-given
Okay, so I can't duplicate the exact OO syntax issues I was having before.
But, from memory, I was finding that by putting the wrong kind of statement
between the type definition and the method declaration, I could thoroughly
upset the compiler --- there was invisible syntax connecting the two together,
and if you I put the wrong thing in between, then things stopped working.

But as I can't duplicate it it's entirely possible I was just hallucinating.

In general I find the OO syntax desperately confusing. It feels like it's been
shoehorned in on top of the existing record and procedure syntax, and it's
never clear exactly what anything does. e.g. you need to suffix the object
type with 'class in methods in order to make them _non_ dispatching, but you
need to suffix the object type with 'class in variable types if you want them
to dynamically dispatch? That's not a happy choice.

(Case in point: I've just spent 20 minutes trying to refresh my memory by
making this code snippet work. And failing. What am I doing wrong?
[http://ideone.com/6iPdYF](http://ideone.com/6iPdYF))

Incidentally, re getadanow.com: that's really nice! And it's not pointing at
the Adacore compilers, either; beware of these, as their standard library is
GPL, _not_ LGPL, which means you can't distribute binaries built with them.
(The standard GNAT version is fine.)

~~~
msbarnett
> But as I can't duplicate it it's entirely possible I was just hallucinating.

There's a thing where if you declare a type A, and then a derived type B,
methods on A have to be declared before type B gets declared, because B's
declaration "freezes" A. I think it's mostly a single-pass optimization that
might have made sense 20 years ago but is meaningless in an era of gigabytes
of RAM.

> (Case in point: I've just spent 20 minutes trying to refresh my memory by
> making this code snippet work. And failing. What am I doing wrong?
> [http://ideone.com/6iPdYF](http://ideone.com/6iPdYF))

The specific error message is: you declared a package, which is basically a
header in C parlance. You declare signatures in it, not method bodies. Method
bodies go in package bodies. You were conflating the package and the package
body.

And then from line 19 onwards you were using the package name where you wanted
to be using a class name. I cleaned it up a bit and made it work:
[https://gist.github.com/mbarnett/9c6701fe74524a6df522](https://gist.github.com/mbarnett/9c6701fe74524a6df522)

~~~
david-given
> There's a thing where if you declare a type A, and then a derived type B,
> methods on A have to be declared before type B gets declared...

Yes, that sounds very familiar.

> The specific error message is: you declared a package, which is basically a
> header in C parlance.

Oh, FFS. That snippet is not, in fact, pointing at the piece of code I was
actually asking about --- ideone changed it when I wasn't looking. The one you
saw is unfinished and broken.

 _This_ one is the one I was meaning:
[http://ideone.com/skZRIb](http://ideone.com/skZRIb)

The .Foo selector isn't found; changing it to Foo(object) reports that
apparently Foo isn't a dispatching method on MyClass1... which makes no sense,
because this is the same code as you had. My suspicion is that there's
something magic about declaring classes in packages?

~~~
msbarnett
> which makes no sense, because this is the same code as you had. My suspicion
> is that there's something magic about declaring classes in packages?

Yeah.

Dispatching methods on a type consist of the type's "primitive operations".
The Ada 95 Rationale spells it out: "Just as in Ada 83, derived types inherit
the operations which "belong" to the parent type - these are called primitive
operations in Ada 95. User-written subprograms are classed as primitive
operations if they are declared in the same package specification as the type
and have the type as parameter or result."

It seems like a wart that you're not in an "anonymous" package in situations
like your example, but I also guess it probably doesn't come up much in "real"
programs.

------
Animats
Good points.

The author points out that channel teardown is hard. He's right. Figuring out
how to shut down your Go program cleanly can be difficult, especially since
calling "close" on a closed channel causes a panic. You have to send an EOF on
each channel so the receiver knows to stop. When you have a pair of channels
going in opposite directions between two goroutines, and either end can
potentially initiate shutdown, it gets messy.

At least in the original implementation, "select" for more than one option was
really slow and complex. The single-input case was handled efficiently with
generated code, but for N > 1, a very general library mechanism with several
heap allocations for each message was used. This means having both a wait for
data and a timeout in a select puts you through the slow path. Not good.
Someone did an analysis of existing programs and found that N=1 was most
common, N=2 was reasonably common, and N>2 was rare. N=2 needs special case
support.

QNX interprocess messaging has a similar architecture. But they don't have the
panic on close problem, and timeout is handled efficiently. So you can
generally shut things down by closing something. As each process is notified
about the close, it closes any channels with which it is involved, even if
some other process has already closed them. The closes thus cascade and
everything shuts down cleanly. Processes that time out at a message receive
check to see if the rest of the system is still running, and shut down if it's
not.

Go's "share by communicating" would be more useful if Go had Rust's borrow
checker, so you could share data without races. Yes, Go has a run-time race
detector, but that's only useful if races are common enough that they occur
during testing.

~~~
r_sreeram
> N=1 was most common

Why would somebody use "select" for this at all? I.e., if you were going to
write:

    
    
      select {
        case send/receive statement:
          statement
          ...
      }
    

Why not just write:

    
    
      send/receive statement
      statement
      ...
    

What am I missing?

~~~
spenczar5
I believe the GP was referring to select-with-one-case-and-a-default, like

    
    
      select {
        case <- ch:
        default:
      }

------
travjones
This was a well-written and entertaining post. It represents the kind of self-
reflection every programming community should encourage. Too often are devs
zealously supportive of their language of choice without considering
thoughtful critiques that could make their chosen language even better, and/or
present an alternate way of looking at things that makes one better at
programming in general.

------
divan
I didn't get the point of example with Game and Player. The code behaves
exactly how it's told to. If you need some logic to handle conditions where
all players have been disconnected - you should implement it, no matter how.
Maybe you want to wait for some time for a new players and teardown only after
this timeout. Or, maybe, you want to reuse this game object, moving to some
kind of pool (like sync.Pool). Or, perhaps, you really want to wait forever
for returning players. It's not 'mutex vs channels' example in any way.

It's not 'fix goroutine leak' it's "write the logic you want", it's that
simple.

Next, channels are slow, really? Send-receive operation on unbuffered channel
typically takes around 300ns. Nanoseconds. 300 nanosecond in exchange of nice
and safe way to express concurrent things - I wouldn't even call it a
tradeoff. It's not slow at all in vast majority of cases. Of course, if you
write that software that do care about nanoseconds and channels becomes your
bottleneck - congratulations, you're doing great, and you probably have to
switch to C++, Rust or even Assembler.

But, please, don't mislead people telling them, that channels are slow. They
could be slow for your exact case, but it's not the same.

I don't really get the tone and arguments of the article. Some of the points
are totally valid, but they easily falls into the 'hey folks, be careful about
this small thing you may misunderstand at the beginning' category. Pity.

~~~
nemothekid
>* Of course, if you write that software that do care about nanoseconds and
channels becomes your bottleneck - congratulations, you're doing great, and
you probably have to switch to C++, Rust or even Assembler.*

Thats ridiculous. I could switch my entire language... or I could just use a
lock?

First off, looking at Tyler's post, he measured unbuffered channels taking
2200ns vs 400ns for the lock solution - a 5x speed up. That is a large gain,
especially when dealing a program that may have high lock contention. Switch
from Go to C++ or Rust my not even gain you that much in terms of throughput -
they are both compiled code and moving to either language will only mainly
alleviate of magic STW pauses - acquiring a lock likely won't be any faster.

Second, in the point of Game and Player, the logic to handle conditions where
players disconnect is still simpler to implement with locks - its 2 lines, and
there is no need to bring in sync.Pool, or introduce arbitrary timeouts.

Channels are slower than locks. In more complex applications, channels are
easier to reason about than locks, but those tends to be in cases where you
care more about message passing than state synchronization.

~~~
pcwalton
> Switch from Go to C++ or Rust my not even gain you that much in terms of
> throughput - they are both compiled code and moving to either language will
> only mainly alleviate of magic STW pauses

That is not the only performance-related difference between those language
implementations. It's not even the most significant one.

For instance, there is a large difference between a compiler with LLVM's
optimizations and one without an SSA backend at all.

~~~
ngrilly
The new Go SSA backend was merged into tip a few days ago:

[https://groups.google.com/d/topic/golang-
dev/49VaiLCDbeQ/dis...](https://groups.google.com/d/topic/golang-
dev/49VaiLCDbeQ/discussion)

------
Jabbles
Effective Go has always said:

 _Do not communicate by sharing memory; instead, share memory by
communicating.

This approach can be taken too far. Reference counts may be best done by
putting a mutex around an integer variable, for instance._

[https://golang.org/doc/effective_go.html#sharing](https://golang.org/doc/effective_go.html#sharing)

~~~
poizan42
Reference counts are best done using interlocked increment/decrement
primitives.

~~~
chrisseaton
I wonder if there are any compilers which can replace

    
    
        mutex.lock { x++ }
    

With a 'lock xaddl x 1' instruction.

~~~
mike_hearn
It's not quite the same thing but recent JVMs can translate synchronised
blocks into Intel TSX transactions, which means multiple threads can run
inside the lock at once, with rollback and retry if interference is detected
at the hardware (cache line) level. So yeah .... almost. But it's fancy and
cutting edge stuff.

------
advanderveer
I see channels as an architectural option when it comes to structuring the
communication between components of my software. Mutexes are another option
that are more effective in situations where multiple threads may access the
interface of a single structure. E.g I use channels to distribute os.Signals
througout my software and a mutex for making a "context" structure thread
safe. Right tool for the right job

~~~
jemfinch
Even when that's the case, it's rare that fixed-size buffered or unbuffered
channels are really the best option for communication between different
components of your software. A simple mutex-guarded queue is easier to begin
with and easier to evolve when requirements change. You can prioritize queued
work trivially and transparently; you can add batch processing, monitoring,
and resolve other production issues without any undue refactoring: it can all
be encapsulated behind your mutex-guarded queue.

It's really quite a pity that Go's channel syntax treats channels as unique
snowflakes, rather just being sugar for calls into an interface that would
allow the underlying channel implementation to differ based on software needs.

~~~
richard_todd
That's an excellent example (in a long list) of things that would be possible
with generics, or even parameterized packages. They could have provided an
interface Channel[T] with syntax sugar if desirable. But as it is, everything
in Go that can handle multiple types has snowflake status.

------
dilap
Funny timing for me -- last Friday I rewrote some code from channels to
traditional sync primitives (to the code's improvement), and I was musing in
my head that while everyone always says "don't communicate by sharing, share
by communicating, yada yada," it doesn't seem to work out that way in
practice.

I think the article is well-written, and clearly comes from a place of deep
experience and understanding. Good stuff.

~~~
gort
I had a similar experience in the opposite direction. Two weeks ago I moved
some code from a mutex-based design (including a map of string to mutex, which
itself needed to be behind a mutex) to channels, and I love it, though the
result seemed about 10% slower.

I guess the message is: everything has its place; don't make a straight-jacket
for yourself.

~~~
dilap
No doubt it's quite easy to go down ratholes using mutexes and whatnot. It's a
very low-level way of synchronizing.

The OPs critques of the specific design of channels as implemented by Go seem
on-point to me.

------
drdaeman
Offtopic: that animated image is literally nauseating. Consider removing it,
or making it animate just once and then halt. It was meant to be "fun" or
whatever but, seriously, I wasn't able to read the text when it looped over
and over in the corner of the eye.

~~~
jtolds
Alright, alright, I froze a bunch of the animated gifs. There were too many, I
get it.

------
hacknat
I think I've just come to accept that sychronization is the pain point in any
language. It's callbacks, promises, and the single event loop in nodejs. It's
channels in golang.

No one can come up with a single abstraction for synchronization without it
failing in some regard. I code in go quite a bit and I just try to avoid
synchronization like the plague. Are there gripes I have with the language?
Sure, CS theory states that a thread safe hash table can perform just about as
well as a none-thread safe, so why don't we have one in go? However...

Coming up with a valid case where a language's synchronization primitive fails
and then flaming it as an anti-pattern (for the clicks and the attention, I
presume) is trolling and stupid.

~~~
yetihehe
> No one can come up with a single abstraction for synchronization without it
> failing in some regard.

Erlang did. Or at least it's as close as possible.

~~~
hacknat
I'm not saying Erlang isn't great, but if you need to pass a large
datastructure around between Erlang processes then copy message passing starts
to be a lot and you need to share memory. You can do it in Erlang, but I'd
hardly call it great, and you're avoiding the sync primitive that Erlang
offers.

~~~
catnaroek
How about Rust's “share by transferring ownership”?

(0) In the general case, whatever object you give to a third party, you don't
own anymore. And the type checker enforces this.

(1) Unless the object's type supports shallow copying, in which case, you get
to keep a usable copy after the move.

(2) If the object's type doesn't support shallow copying, but supports deep
cloning, you can also keep a copy [well, clone], but only if you explicitly
request it.

This ensures that communication is always safe, and never more expensive than
it needs to be.

\---

Sorry, I can't post a proper reply because I'm “submitting too fast”, so I'll
reply here...

The solution consists of multiple steps:

(0) Wrap the resource in a RWLock [read-write lock: [http://doc.rust-
lang.org/std/sync/struct.RwLock.html](http://doc.rust-
lang.org/std/sync/struct.RwLock.html)], which can be either locked by multiple
readers or by a single writer.

(1) The RWLock itself can't be cloned, so wrap it in an Arc [atomically
reference-counted pointer: [http://doc.rust-
lang.org/std/sync/struct.Arc.html](http://doc.rust-
lang.org/std/sync/struct.Arc.html)], which can be cloned.

(2) Clone and send to as many parties as you wish.

\---

I still can't post a proper reply, so...

Rust's ownership and borrowing system is _precisely_ what makes RWLock and Arc
work correctly.

~~~
hacknat
What if you want multiple readers at once, and a writer thrown in once in a
while?

Edit:

Okay, my point was that the sync primitives of most languages alone can't save
you and you're using RWLock in your example, so clearly ownership by itself
doesn't solve everything, right? That's the point I'm trying to make.

Edit2:

Hmm, I'll have to check that out. I don't know that I would call Rust's
ownership model super easy to reason about, but it is nice that the compiler
prevents you from doing so much stupid $#^&.

~~~
azth
> Hmm, I'll have to check that out. I don't know that I would call Rust's
> ownership model super easy to reason about, but it is nice that the compiler
> prevents you from doing so much stupid $#^&.

It's much better get compile time errors than deal with very hard to reproduce
data races.

~~~
kazinator
Only, as usual, in situations when all else is equal.

By the way, on a related note, data races themselves are easier to reproduce
than the _visible negative consequences_ of those races on the execution of
that program. That's the basis of tools like the "Helgrind" tool in Valgrind.
That is to say, we can determine that some data is being accessed without a
consistently held lock even when that access is working fine by dumb luck. We
don't need an accident to prove that racing was going on, in other words. :)

~~~
catnaroek
> By the way, on a related note, data races themselves are easier to reproduce
> than the _visible negative consequences_ of those races on the execution of
> that program.

Perhaps, but a data race by itself isn't sufficiently loud to catch my
attention (no idea about yours), unless it _consistently_ has visible
consequences during debugging - preferably not too long after the data race
itself takes place.

> That is to say, _we can_ [emphasis mine] determine that some data is being
> accessed without a consistently held lock even when that access is working
> fine by dumb luck.

By “we”, do you mean human beings or computers? And, by “can”, do you mean “in
theory” or “in practice”? Also, “only when we're lucky” or “reliably”?

> We don't need an accident to prove that racing was going on, in other words.

What I want to prove is the opposite - that there are _no_ races going on.

------
fpoling
The article presents very similar arguments to those that I read in a book
from 1982 or so. It discussed channels in Ada and pointed out that without
super smart compilers that would turn channels into mutex operations the code
using channels would be slower and more complex due to the need to create an
extra threads.

Base on that I can predict that in 2050 I will also read an article discussing
channels in yet another language and advocating using mutexes instead...

------
_ph_
I am not a Go veteran, but can see where this article is not helpful. Yes,
channels are not a solve-everything. That is, why the Go library also contains
mutexes etc. The game serving example could have been fixed by adding a
channel to signalize that the game is finished. The game runner function
should listen on the "scores" and the "done" channel with a select. Or, not
use a channel at all. The channels are great, when you just want a completely
safe method of communicating between goroutines, as long as the communication
reasonably falls in the "streaming" behavior of the channel model.

------
woodcut
I find it hard to read something when the language used is so patronising.

~~~
feathj
The language is one thing. I am so tired of trying to read articles riddled
with gifs.

~~~
coldtea
I'm so tired of reading negative comments about entirely subjective (others
might appreciate the gifs) and totally skippable if one doesn't like them (you
can also ignore them) elements of a good post.

~~~
woodcut
Criticising how information is communicated is wholly valid.

~~~
coldtea
Criticising how information is communicated yes.

But saying "I hate articles riddled with gifs" is far from Marshall McLuhan
and Edward Tufte.

Especially since it's not some shallow Buzzfeed post, but a detailed technical
explanation of a programming-related issue that the author took time and
effort to write -- which makes complaining about its presentation petty.

The author obviously wanted to lighten it up and add some fun elements. And he
provided his opinion and expertise for free. These kind of comments can mainly
serve to discourage him from writing more, not get him to "improve" his
communication.

~~~
woodcut
alright, point taken.

------
soroso
From [http://go-proverbs.github.io/](http://go-proverbs.github.io/):

Channels orchestrate; mutexes serialize.

------
hacknat
Should have been titled, "Interesting Cases Where Go Channels Fail"...

------
gravypod
In non-critical things (not important to execution speed), is it still
acceptable to use go channels? I'm always weary of using a mutex because then
I have to spend a much larger amount of time checking to see if it will lock.

------
lazyjones
Saving the highscore in a goroutine becomes more interesting if that action
can block or simply take a while, both more realistic occurrences than such
minimalistic examples.

------
hamburglar
Non Go programmer here. Can someone explain the initial goroutine leak that is
being addressed? I don't see the issue.

~~~
divan
It's not actually a leak. It's a program explicitly doing 'run goroutine and
don't care of it anymore'. If the program logic wants this - it's ok. If
author wants it to finish on some condition, but didn't write the condition
code (like in this article) - it's a leak, but it's purely author's mistake.

~~~
jtolds
There is a _deliberate_ leak in the example program. The author was attempting
to illustrate that fixing the leak using just channels would be a challenge.

~~~
courtf
Synchronizing access to a memory address isn't really the use-case for
channels. I think that's fairly well understood by the Go programmers I work
with. This example demonstrates why, but it prefaces the discussion by
implying this is the standard practice, which I think is misleading.

~~~
jtolds
I perhaps communicated poorly, but the point of that section was to try and
explain that the CSP model ( _only_ using channels) was untenable in Go (even
though it doesn't necessarily have to be in general), and that you'd almost
certainly end up not just using channels in a real program, which it seems you
agree with.

------
tptacek
You can express unbounded buffered channels in Go straightforwardly with the
stacked channel idiom.

------
richard_todd
I enjoyed the article and nodded along as I read it. But after, I felt like it
was overstating its case a little. It puts up a toy implementation that kinda
works, and then explains that to make it act correctly in the real world you
have to add uglier code. I can't really see blaming the language constructs
for that... show me a language where that doesn't happen!

I do appreciate that the article tries to deflate some of the hype about
channels that you see when first investigating Go (I know I bought into it at
first). After a little experience, I settled into a pattern of using channels
for large-scale and orderly pipelines, and more typical mutexes and conditions
for everything else. They have strengths and weaknesses, like all tools.

------
f2f
the Clive[1] system uses a fork of the Go language which allows readers to
close channels (I think it's the most significant difference between the
languages, if not the only one):

[http://syssoftware.blogspot.com/2015/06/lsub-go-
changes.html](http://syssoftware.blogspot.com/2015/06/lsub-go-changes.html)

\--

1: [http://lsub.org/ls/clive.html](http://lsub.org/ls/clive.html)

------
pklausler
Channels are great, but I prefer lazy lists.

------
b169118
How can I play the gifs again?

------
GhotiFish
a nice feature of this post would be the ability to click on images to hide
them.

Normally I have ublock to remove distracting elements.

------
elcct
There is probably a large number of developers who think "OMG my Go code
doesn't have any channels and goroutines. Am I doing this right?" If you try
to force a solution that isn't quite right for the given problem, then well,
have fun. Case presented by the author I would naturally program with Mutexes,
as me thinks using channels / goroutines is an overkill for this task.

------
PaulHoule
Pixie dust that makes concurrency problems go away is an antipattern.

------
amelius
This article should have been titled: "Go channels considered harmful" :)

~~~
kbenson
Thankfully it wasn't, and we were spared the ensuing discussion about how
"..considered harmful" is good/bad/overused/misunderstood/causes cancer.

~~~
falcolas
Instead we get the debate of whether "and you should feel bad" is appropriate
in a title.

I don't believe it is, but ultimately click bait is click bait.

~~~
kbenson
Eh, I don't mind click bait titles as long as the article delivers, and the
title isn't too egregious in its manipulation. In this case, I think it's
pretty well understood by most that the title is poking fun, since taking it
truthfully is fairly ridiculous.

