June 23rd, 2024

Three ways to think about Go channels

Channels in Golang are locked, buffered queues for message passing. They integrate with goroutines, select blocks, and more, offering efficient concurrency. Understanding their role and benefits is crucial for Golang developers.

Read original articleLink Icon
Three ways to think about Go channels

The article discusses three key aspects of channels in Golang. Firstly, channels are described as locked, buffered queues, with senders adding to the queue and receivers reading from it. Secondly, channels are part of a broader ecosystem of concurrency primitives in Golang, including goroutines, select blocks, timeouts, tickers, and wait groups. Lastly, the article delves into the concept of message passing through channels, emphasizing the efficiency and convenience of using channels over user-implemented queues due to Golang's runtime capabilities. The piece highlights the importance of understanding channels as a queue abstraction, their integration with other concurrency primitives, and their efficiency compared to alternative concurrency options. It concludes by emphasizing the usefulness of these abstractions in different contexts and invites readers to engage with the Dolt team for further discussions on Golang performance and related topics.

Link Icon 12 comments
By @hedora - 5 months
I’ve worked with a few large code bases that use channels. In all those code bases, they were about as maintainable as GOTO-based control flow, except that GOTO makes you have unique label names. All the channel-based code I’ve seen just has 100’s of call sites like “chan->send()” and “chan->recv()” sprinkled around, and doesn’t even have the discipline to put related senders and receivers in the same source file.

At least old-school syntax like “GOTO foobar_step_23” and “LABEL foobar_step_23” is grepable).

I greatly prefer programs that “color” functions sync/async, and that use small state machines to coordinate shared state only when necessary.

Go technically supports this, but it doesn’t seem like it is idiomatic (unlike rust, the go compiler won’t help with data races, and unlike C++, people don’t assume that the language is a giant foot-gun).

By @cyberax - 5 months
I must admit, I dislike Go channels. They are a hell to debug, as they are unnamed. And they're anonymous, so you can easily get a stacktrace filled with thousands of identical stacktraces that you can't correlate with logs.

Golang needs to have a way to manage the channels better. Naming them and waiting on them would simplify a lot of crusty stuff. Naming is becoming possible, goroutines can already have pprof labels (that are even inherited between goroutines!), so just adding pprof labels to stacktraces will help a lot.

But unfortunately, Go creators are allergic to anything that brings thread-local variables closer.

By @jgrahamc - 5 months
I'll admit that I like Go channels because Hoare was the professor when I was doing my doctorate and so CSP was what I used in my thesis[1], but it's worth understanding what unbuffered channels give you: it's message passing with synchronization. They are very simple to reason about and make writing concurrent code a breeze.

[1] https://blog.jgc.org/2024/03/the-formal-development-of-secur...

By @tapirl - 5 months
> One literal interpretation of Pike's quote is that message passing is different than sharing memory for pedantic reasons. Copying values between sender/receiver stacks is safer than sharing memory. But it is also possible to send values with pointers into channels, so that doesn't really prevent developers from abusing the model. I don't think avoiding shared memory is a top of mind consideration for developers deciding whether to use channels.

I think the point of Pike's quote is that, when a goroutine gets a pointer received from a channel, it gets the ownership of the values referenced by the pointer and other goroutines give up the ownership. This is a discipline Go programmers should hold but not a rule enforced by the language.

By @doawoo - 5 months
Y’all should really give Erlang/Elixir a try… this stuff is so much more trivial to deal with in that ecosystem and it pains me that it doesn’t get as much attention as Go does.
By @liampulles - 5 months
As an avid Go user, I think async/await is probably a nicer construct for most usecases. But Go channels work fine as long as you keep to the basics and documented patterns.

I can recommend making a utility function which accepts a set of anonymous functions and a concurrency factor. I've since extended this function with a version which accepts a rate limiter, jitter factor, and retry count. This handles most cases where I need concurrency (batches) in a simple and safe way.

By @adeptima - 5 months
Well writen. Will recommend to all newcomers from other languages.

As for uncovered topics or part 2 - long running Go channels can be a nightmare.

You need to implement kind of observability for them.

You must find the way to stop/run again/upgrade and even version payload to handle your channels/goroutine.

Common problem with channels overuse is so called goroutine leaks. Happens more often than most devs think. Especially, if lib writers initiate goroutines in init() to maintain cache or do some background cleanup job. It's good to scan all used packages for such surprises.

You might also find concepts like "durable execution" or "workflow" engines down the road.

By @onionisafruit - 5 months
I didn’t realize why channels are part of the language vs a standard library feature until I read this. Now it makes sense that it’s about compiler optimizations with goroutines.
By @parhamn - 5 months
Go channel behaviors are pretty annoying. For one I always forget the panic scenarios (e.g. writing to a closed channel), I feel like the type system could've done more here.

I recently wrote a simple function that maps out tasks concurrently, can be canceled by a context.WithCancel, or if a task fails. The things that cancel the task mapper need to coordinate very carefully on both sides of the channel so that they're closed and publishers stop sending in the right sequence. The amount of switches/cancels/signals quickly explode around the coordination if you too cute on how to do it (e.g. read from the error channel to stop the work).

Frankly I'm not sure I still got it right [1]. And this is probably the most unsettling part. Rereading the code I can't possibly remember the cancellation semantics and ordering of the short mess I created. Now I'm wondering if mutexes would've made for more understandable code.

[1] https://gist.github.com/pnegahdar/1783f0a4e03dc9a3da43478994...

By @DylanSp - 5 months
Channels can be a useful primitive, but without more structure they're tough to reason about. I really wish that the Go team would provide more implementations of common patterns and publicize them; things like x/sync/errgroup are fantastic, and I'd love to see more.
By @JaggerFoo - 5 months
Timely article for me, since I'm thinking of using channels in a real-world application for the first time.