Interactive mini-book on concurrent programming in Go.

398 points•chmaynard•4 days ago•186 comments•

186 comments

SamInTheShell4 days ago
The concurrency and threading in Go just feels like magic compared to every other language. I'm a goroutine addict and I refuse to be rehabilitated.

Just from observations over the years, I don't think there's any other language quite like this, in terms of how things can end up happening in any thread.

hank19314 days ago
How about Erlang or Elixir using BEAM?

Supposedly WhatsApp scaled to serving over 1 billion users with Erlang and BEAM.

RabbitMQ, used by Reddit, uses Erlang and BEAM.

Discord uses Elixer and BEAM.

I just traveled down the BEAM rabbit hole. Fascinating story. The Ericsson Computer Science Laboratory cranked out some amazing products in the early 1990's.

Their goal was five nines of reliability for Ericsson telephone switches.

According to Joe Armstrong (an interesting fellow from Ericsson), the AXD301 ATM switch achieved nine nines over a nine-month period using Erlang and BEAM in 2002. That calculates out to 24 milliseconds of downtime.

CoolestBeans4 days ago
BEAM+OTP is a masterclass in using concurrency to achieve fault tolerance. But Go achieves its "magic" by feeling like the lingua francas of programming, C and C++. Go doesn't make the developer learn too many new concepts. The runtime is self enclosed in the final binary. This commitment to the familiar programming patterns also means it allows for concurrency anti-patterns like shared memory which for Erlang+OTP's design principles is verboten.
SamInTheShell4 days ago
I looked at BEAM about a year or so ago, similar conversation here. I don't think BEAM is the same when you start looking at what part of code is executing in which thread. There's tradeoffs depending on what you're solving for, like Go makes it really simple to distribute your work across threads concurrently, but when you start looking at integrating with stuff, you run into having to do tricks to do things with unshare (ref: docker/podman/containers...) and you haven't been able to integrate into libnss since they started using some "unused linux signal" for concurrency controls (PAM used that signal).
chickensong4 days ago

  Hello, Mike.
  Hello, Joe.
  System working?
  Seems to be.
  Okay. Fine.
  Okay.
asp_hornet4 days ago
Yes! BEAM and OTP is amazing. Concurrency is one aspect and Go has great concurrency primitives, but what about supervision, and recovery and failure modes? Often they’re left to the developer as per Go’s philosophy which I think makes sense. OTP offers a lot of solutions to this.

I think Go and the BEAM family languages are both great.

fzeindl4 days ago
Yes. Once you know Erlang/Elixir and BEAM you realize it is at least a local optimum in languages/VMs.

Truly something else if error handling is built into the language as a default case, not an … exception.

lukaslalinsky3 days ago
I was amazed how well are goroutines integrated into the language when I saw the first videos from Rob Pike. Then I actually started using Go for concurrent code, and noticed one thing, it's extremely easy to leak goroutines. There is no proper way to cancel them, they need to cooperate via select/context. Go developers eventually learn hacks to deal with it, but the simple go+chan style of programming style you see in tutorials is usually not safe. I still consider Go a remarkable piece of software. The runtime really doesn't have any seriously bad edge cases, it just works. But as a developer, I now prefer a slightly more explicit approach to concurrency. I've spent the last year developing an async runtime for Zig and I'm now more comfortable writing concurrent code in Zig than I was every using Go. I have more options for how to handle closed channels, I can cancel any operation, etc.
delifue3 days ago
One reason it's easy to leak goroutine is that channel producer blocks waiting on consumer, once channel consumer exits the producer goroutine leaks. Go doesn't allow consumer to close the channel.

In Rust when receivers all drop, the producer will error instead of blocking, so Rust is better in this aspect.

SamInTheShell3 days ago
Never really thought about this but it seems to me that if you want to launch separate processes? Goroutines are built around functions, so you're just stuck with function semantics. If you want an entire process, you just invoke self with a feature flag on your binary and control a subprocess. If you need to communicate you establish your own message passing channels with STDIO or something.

I don't feel this is hacky or even a work around. Just different promises on what goroutines are vs threads/concurrency/processes in other languages.

Exit and panics are promised at the process level in Go.

throwaway8943453 days ago
> There is no proper way to cancel them, they need to cooperate via select/context

Isn’t this also true of threads? I know you can usually cancel them from a thread handle, but that kills the thread ~immediately without cleaning anything up, right? Presumably you pretty much always want cooperative cancellation?

kccqzy3 days ago
Yup. A popular way to get proper cancellation is to build exceptions into the language and specifically async exceptions so one goroutine can throw an exception into another goroutine. And Go does not have exceptions. Doing so would require all regular Go code to be exception safe, and really requires some form of try/finally or RAII but not defer. Anyways exceptions are quite far from the Go creators’ vision of the language.
tym03 days ago
We're heavy user of Go at work. Go also makes it way to easy to write bad concurrent code and hard to write good one.

Stick to err/wait group and go routines and it's OK. Any PR with a channel or mutex I'll assume the author made a mistake.

lolpython3 days ago
Asking as an outsider to Go. How do you communicate between threads without a channel or mutex?
neillyons3 days ago
Elixir concurrency feels like magic with async_stream https://elixir.hexdocs.pm/Task.html#async_stream/5-options

  [
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/3"
  ]
  |> Task.async_stream(
    &Req.get!/1,
    max_concurrency: 3,
    ordered: true,
    timeout: to_timeout(second: 2)
  )
  |> Enum.to_list()
osigurdson4 days ago
>> in terms of how things can end up happening in any thread

Doesn't that describe pretty much any green thread style concurrency implementation.

dlisboa4 days ago
No. Preemptive scheduling plus M:N mapping combination that Go has is not common in other major implementations.

Other languages and their implementations of green threads usually have cooperative scheduling or M:1 mapping

voidfunc4 days ago
Ive been writing Go for over a decade and I still feel like I never quite "got" channels. Every time I use them I need to go consult the manual, and none of the patterns feel obvious which is weird considering the rest of the language feels very obvious.

Too many years of Java and managing Threads and Runnables probably rotted my brain.

vrosas4 days ago
Channels are honestly one of the most over-used things in Go. I've been writing Go professionally since 2015 and I honestly rarely use them. Programmers new to Go love to shovel them in everywhere because "why use Go if you're NOT going to use channels?" and I have to say sorry, no - write it serially, then determine if it breaches your SLOs, THEN determine if concurrency fixes it.
silisili4 days ago
Using Go since 1.0, agree wholeheartedly. Newcomers read the docs and start throwing channels everywhere because why not.

I always ask/tell people to write without channels, and only add them when you have justification for doing so. That leads to much more sane code.

One pattern I see often because random blogs mention it is starting X long lived goroutines, then passing them data via channels, then receiving responses via channels, then handling. In my experience, it's 100x less error prone to just use a semaphore to start a goroutine per data, and have them do their own handling. No channels involved.

melodyogonna4 days ago
Yup. Go maturity is realising how little you need to use channels and Goroutines. You probably just need a setup in one place, like in front of incoming requests ... which using net/http already does for you.

Spamming them all over the place is a red flag imo

mugul4 days ago
Very interesting feedback. I'm a Go newbie and the goroutine/channel duality sounds delightful from where I stand, but once again I have no professional experience with Go yet, only sample programs to get used to the language.

One question though: your advice is to write things serially first before moving to concurrency, which for me is general programming common sense, but would you argue that once you start writing concurrent code then channels are not well suited compared to "good old" sync primitives (mutexes, etc.)?

cejast4 days ago
I’d say it’s important to understand how they work but I also rarely find myself reaching for channels. I see more usage of wait groups and mutexes, but even then you can build abstractions around these in a way that can be reused without having to touch them again.
amomchilov3 days ago
Bolting on concurrency after-the-fact is notoriously difficult to do.
liampulles3 days ago
It depends really on what you actually want to do. I tend to make a few helper funcs for different kinds of things I want to do. For example, a helper funcs to accept anonymous job funcs and collect output. Then you can compose programs out of those higher level blocks.
pjmlp4 days ago
Channels are basically messaging queues in Java.
eikenberry3 days ago
Try writing CSP style code and see how channels slot in. That is the design they had in mind.
za3faran4 days ago
Arguably, Java's virtual (green) threads managed through structured concurrency and futures is a superior approach.
Mawr4 days ago
Arguably, being 10 years late to the party is pretty bad.

Just how Go adding generics to the language didn't magically fix the billions lines of non-generic Go code, adding virtual threads to Java didn't update its entire ecosystem to take advantage of them.

Meanwhile, the entire Go ecosystem from the beginning took advantage of goroutines, so all code you'll ever interact with will have excellent support for them.

SamInTheShell4 days ago
Depends on what you're solving for. Message passing is fine. That's all channels does.
jatins4 days ago
> superior approach

superior how -- What does it do better over Go channels in your opinion?

kccqzy4 days ago
For many people, besides learning what you should do, it is more helpful to read anti-patterns and things you should not do in Go, and none is better than this article about data race patterns in Go: https://www.uber.com/us/en/blog/data-race-patterns-in-go/
jeffrallen3 days ago
This is fine, but it's too bad it did not mention the cardinal rule of goroutines on prod, which is "before starting a goroutine make damn sure you know how it will stop".

Goroutine leaks in prod are no laughing matter. They are difficult to debug without killing the process, and that's only useful if you are sure you're going to get stderr to get the full traces of all goroutines.

dzogchen3 days ago
Ahh you got me. Finished the first chaper of the 'free online' Gist of Go book, then in the second chapter it turns out the first chapter was a freebie.

Read the full thread on Hacker News →

Related stories