Hacker News

Top stories

Live mirror
30 storiesupdated just nowView source snapshot
  1. Flip Fluid on Flip Dots (mitxela.com)
    12comments
  2. OpenAI Feared "Optics" of what might appear on Hacker News (authorsguild.org)
    330comments
  3. "As a Language Model": Chat Template Switches LLM Self-Referential Voice (arxiv.org)
    65comments
  4. Does Georgism work? Five years later (astralcodexten.com)
    299comments
  5. Go Concurrency Distilled (antonz.org)
    113comments
  6. PipePipe: NewPipe hard fork implementing SponsorBlock (github.com/infinityloop1308)
    240comments
  7. Fakecloud: Local AWS cloud emulator for integration tests (fakecloud.dev)
    3comments
  8. The internet discovers TLA+. Now what? (reasonable.io)
    27comments
  9. DeepSeek Elastic Compute (DSec) (arxiv.org)
    92comments
  10. Finally, A True Blue Rose Exists (sciencenews.org)
    11comments
  11. Show HN: Reladraw – A diagram language where you decide where to place things (github.com/reladraw)
    90comments
  12. ASML says it sold 'absolutely nothing' in Europe in 2026 (tomshardware.com)
    727comments
  13. Biology might not be quantum, but its math is quantumlike (quantamagazine.org)
    34comments
  14. A searchable library of forgotten public-domain film clips from 1915 onward (movingimagearchive.com)
    26comments
  15. Exploding variance of means of exponentials: least-squares to the rescue (francisbach.com)
    —discuss
  16. Fifteen years later, the Apple Cards origin story (lexontech.org)
    105comments
  17. Evolving programming languages in the AI era (dashbit.co)
    75comments
  18. An agent used DNS to reach an external chatbot (alignment.openai.com)
    119comments
  19. How I changed teaching after AI managed to do all my homework assignments (thelastsoftwareengineer.substack.com)
    213comments
  20. Teaching a World Model to Play Pokemon (nostalgia.dev)
    17comments
  21. Drawgent: Coding agent on a live Excalidraw canvas (tangled.org/yanndegat.tngl.sh)
    43comments
  22. Meta Blocks President Lula's Facebook Page, Campaign Ads 2 Weeks from Election (reddit.com)
    208comments
  23. Accelerated Out of Core Shuffling (quasiben.github.io)
    —discuss
  24. How to keep enjoying programming in a world of LLMs (haskell.org)
    297comments
  25. Promising discoveries about the potential for life on one of Saturn’s icy moons (fu-berlin.de)
    41comments
  26. Turning GLM-5.3-Flash into a Jev-like decision model (privatemode.ai)
    45comments
  27. Reverse-engineering the Intel 8087's tangent algorithm: more than CORDIC (righto.com)
    11comments
  28. What is the size of Yemen? (2024) (theborys.substack.com)
    70comments
  29. Generate fonts where every LLM token is the same width (mesh.host)
    15comments
  30. Modern Object Pascal Introduction for Programmers (castle-engine.io)
    80comments

Go Concurrency Distilled

279 pointsby 23h agoantonz.org
113 comments
13h agoHN ↗

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.

12h agoHN ↗

Managing channels and making sure they are closed just once is quite messy compared to other languages. The Go channel axioms[1] don't make much sense: why does closing a channel multiple times panic, but reading from a closed channel returns a zero value?

Kotlin gets this right. On send/receive, you can use trySend or tryReceive if you want to avoid exceptions. Considering Kotlin also has coroutines and structured concurrency, concurrency in Kotlin feels more ergonomic to me than Go. At least if you want to get concurrent code with least amount of bugs and not just least amount of extra keywords.

[1] https://dave.cheney.net/2014/03/19/channel-axioms

12h agoHN ↗

Yeah, channels are the main pain point. In addition to the axioms being simply weird (because it's an easy set to implement), another major problem is that you're essentially forced to use them because they're the only things that can work with `select`, and that's the only reasonable option for many operations. Especially if you touch other code, like the stdlib.

That and the lack of tooling around mutex usage / concurrency correctness. The race detector is legitimately excellent and every language needs it, but it can only catch races that you trigger in tests/builds with it enabled, and few projects write anywhere near sufficient concurrent tests to catch issues in practice. There isn't even a "this var claims to be protected by lock X, but it is not held [here]" lint, or "this var is atomic but used non-atomically [here]" (though this one is significantly less of an issue with generics, as safe zero-cost abstractions now exist).

10h agoHN ↗

Go doesn't monomorphize, so benchmark your generics- they might not be zero cost

9h agoHN ↗

Go monomorphizes quite a lot, so that's mostly incorrect - it'd be relatively true for Java, for comparison, ignoring Graal. https://github.com/golang/proposal/blob/master/design/generi... there are only exceptions when you instantiate multiple different types that share an underlying type/layout, all other cases (including different types) are monomorphized. E.g. `type x struct{a int, b float32}` and `type y struct{b int, a float32}` share codegen, but if you even just swap the type order (float32 then int) they wouldn't.

The primitive generic atomics in the stdlib don't run into those details, so you really do get pretty much exactly the compiled code as what you'd write inline by hand:

In particular, fundamentally different built-in types such as int and float64 are never in the same gcshape. Even int16 and int32 have distinct operations (notably left and right shift), so we don’t put them in the same gcshape.

12h agoHN ↗

Once I understood the idioms of Go channels they make sense, but those axioms, while true, aren't the idioms. The idioms would be something more like:

Channels are all intrinsically multi-producer, multi-consumer, and unbounded in size (which is to say, they can carry an indefinite number of messages, not related to channel buffering), but you should still know what the characteristics of your channels are, namely, single or multiple producer and consumer and whether there's some sort of bound on the number of messages. Particularly because you should only ever close a channel if it is single-producer and you are the producer.

It is OK to only use a fraction of a channel's power. For instance, a single-producer, single-consumer channel that is guaranteed (by code, not type system) to only ever have either 0 or 1 messages sent on it is a fairly common pattern.

Never just buffer a channel blindly to try to fix a problem. You should only ever buffer a channel with a size that corresponds to something particular; I know this may receive exactly N messages, 1 from each of N threads, and I want to decouple the possibility the receiver will give up early without hanging the producers, or something like that. Never just slap down a "10" or something and hope it makes things better. The vast majority of channels should be unbuffered.

Putting those two together, the correct way to tear down complicated structures after an error or something is often a channel whose sole purpose is to indicate the liveness of the system in question. In any even remotely modern Go, that should actually be a context.Context and not a channel, which is still basically "a channel with a defined close mechanism" under the hood but adds some other features that are almost always useful at some point.

The reason for all of the above is the select statement. You can in some sense look at "select" as the dual of the channel (being a bit free with the term "dual" here) and consider its functionality as the functionality the Go runtime is actually trying to provide you, from which the characteristics of channels are derived. From this point of view it is then trivially obvious why sends and receives to nil channels block forever... "block forever" is the channel-focused way of seeing the dual statement "the select statement will never select this channel". Some of the other details of channel behavior make more sense if you view them from the select side of the coin.

From this we can also derive a rule of thumb in Go, which is, if your "concurrency" is never going to be involved in a select, it probably doesn't need to be a channel. For example, a simple atomic counter really shouldn't be wrapped behind a channel with a goroutine reading from it or something, just use atomic integers. I have a number of mutexes in my real code. However, never ever take more than one mutex at a time. As soon as you feel like you need to do that, switch to channels, and a proper architecture that uses them somehow to do whatever it is you are trying to do.

(Trying to take multiple mutexes at a time is what led to threading hell in the 1990s. Contrary to popular belief, not just the mere act of threading, but the attempt to do so based on taking multiple mutexes, which at the time was thought to be the only technique available by a lot of the community, leading "threading" to take the heat for what should have been laid at the feet of "taking lots of mutexes at a time in one thread".)

I don't know much about Kotlin, but your cite of "trySend" and "tryReceive" makes it sound like you can do that on only one channel at a time. The fundamental thing about Go channels is that they can be put into select statements which can atomically send from or receive from multiple channels at a time, guaranteed to select exactly one of the possible outcomes. Many "I implemented Go concurrency in X" (often C) flop here. Some kind of queue than can be sent and received on is ubiquitous. Go channels aren't unique, because by the time Go came around pretty much every primitive had been tried somewhere, but it is to my knowledge the only langauge that lifted channels up into the language itself and made them first class.

But stepping up one level of abstraction, "lifting up a particular concurrency primitive to the language level" is not itself anything particularly special and I'm not claiming it is. For instance BEAM had a very particular concept of "mailbox" that it had lifted up into the language and runtime around 15 years earlier, which I have compared and contrasted before here: https://news.ycombinator.com/item?id=34564228 which is, overall, a richer concept than Go's channels, particularly because of its ability to pluck messages out of the mailbox out of the receiving order. Whether that richness is a good thing is something that could be debated a lot.

3h agoHN ↗

It’s not the first time I see you explaining Go concepts at this level of abstraction, focusing on “why” of the design. I feel like official docs are often like “here’s the API, use it”, and I often leave with the thought that it’s designed for the ease of the person doing the implementation, not the user. Thank you.

9h agoHN ↗

why does closing a channel multiple times panic, but reading from a closed channel returns a zero value?

Not only that, but writing to a closed channel also panics. You need to close it exactly once from the sender side, and then somehow differentiate on the receiving side between an explicit zero value being sent, the channel being empty but not closed, and the channel being empty but not closed. It's not clear to me how this is possible without either using another channel (and then basically repeat the same problem on that new channel) or use some sort of shared memory like an atomic bool, at which you're no longer purely message passing.

I don't have any qualms with shared atomic primitives for synchronizing concurrency, but it's kind of weird that everyone talks so much about goroutines and channels when channels have such a weird design. Needing to use a separate mechanism to circumvent completely avoidable design issues for anything more complex than "never close the channel" does not seem particularly praiseworthy to me.

5h agoHN ↗

The easiest way to deal with receiving from a channel is using range over it in a separate goroutine. The for range finishes only when the channel was closed and all values were read.

Of course, that won't work if you want to receive from several channels in the same goroutine. For that you can use select with receive assigning to two values and the second one is set to false if channel is closed.

So, I never really had issues on receive side, I agree with the send side, though. The solution I used when sending from multiple goroutines is to wait for the senders to be finished in the "main" goroutine and only the close the channels.

But yeah, it does require some thought to be put into how this is all organized.

12m agoHN ↗

The solution I used when sending from multiple goroutines is to wait for the senders to be finished in the "main" goroutine and only the close the channels.

How do you know when they're finished though? It seems like you're need to have an additional channel or atomic boolean per goroutine for this, which just increases the amount of organizational burden.

12h agoHN ↗

Go and Julia are fun languages.

In production, Go has proven solid for several years. It is best when used with the native code people ported.

There are only two issues I encountered:

1. getting the legacy ancient C source meta-circular Go compiler working to port the Go boot-strap compiler upgrade chain is a kick in the pants. However, once it is on a architecture it has proven rather resilient.

2. memory limited systems can develop reliability issues, as Go programs will often ungracefully throw hard to diagnose unrelated errors during each crash. A good metric is 3:1 of your average load as a safety margin (if you see 2GiB in average RAM use, make sure to over-provision the host with 8GiB RAM etc.)

Other than the above short list of edge cases, if you join a pure Go project it is usually pretty reliable. Most community folks interested in the language seem fairly competent at building stuff that is fun. =3

8h agoHN ↗

I found Go memory issues easier to solve than Java issues. You can see an example with etcd used in Kubernetes. I had to enable performance profiling in etcd to identify why it was eating up all the memory. It led me to a specific partition of keys that tracked back to a specific object type in Kubernetes.

It was literally enabling a flag and running some commands to do some really quick exports.

Dealing with the JVM though, heap dumps are slow to process and the UI I had to download was very clunky. I don't know if there are better tools, but even if there are the path to just doing it isn't straight forward.

7h agoHN ↗

The potential of Java/OOP died with Sun as far as I am concerned. =3

3h agoHN ↗

You might want to stay concerned, as you are more than 2 decades outdated then.

2h agoHN ↗

Don't let Googles necromancy fool you, most of the use-cases have been deprecated. Have a great day =3

Rule #23: Don't compete to be at the bottom, as you just might actually win.

8h agoHN ↗

if you see 2GiB in average RAM use, make sure to over-provision the host with 8GiB RAM

in this economy?

11h agoHN ↗

Others have mentioned the main issues, but to add; you often end up writing “ugly” code to do basic concurrency operations. Often setting up channels or workgroups then a `go func {}(…); wg.Wait();` just feels wrong and makes you thing “I must be doing something wrong, there must a better way, but that’s IS the way. It’s just go syntax quirks at the end of the day, and makes you appreciate go’s simplicity over high abstractions.

In my experience the main hurdle was getting developers on the team onboard with go’s way. It felt like swimming upstream for my 6 year stint in go. I was in a very Java heavy “enterprise” but we were writing a kubernetes operator and I pushed to use golang because (a) I liked it, and (b) it was 2019 and the entire kubernetes ecosystem was primarily go.

To me golang was very simple and I drank Rob Pike’s and Google’s narrative of how easy it’s to get a competent “compute science major in college”-person to pick up go. What I experienced was a form of “you can’t teach an old dog new tricks”. Lazy (and I hate to use this word) developers who gotten so used to frameworks and IDEs doing all the heavy lifting for them in Java or C# had 0 appetite forgetting all the questionable patterns they learned over the years and adopt Go’s simplicity. It was very frustrating at time, yet gave me a good eye for the actual skilled talent in the organization vs the average enterprise developer persona.

1h agoHN ↗

who gotten so used to frameworks and IDEs doing all the heavy lifting for them

It's almost like those frameworks then achieved their job. Why do you assume you can write better code than what was iteratively refined over years, especially when it's usually not even directly related to any kind of business goal you may have?

8h agoHN ↗

To the concurrency/threading; no? (there might be an "it depends situation somewhere idk about)

But Go itself comes with it's own runtime built into the final binary. It doesn't work well in some use-cases, I mentioned some of the draw backs in a couple other comments if you want to dig those up.

Also I saw some of the other comments. Channels are ultimately just used for message passing and aren't that complicated. You also can use mutexes or some other locking pattern. There's some primitive atomic structures available that solve some use-cases preventing you from even have to having to really deal with working between goroutines.

12h agoHN ↗

I learned Haskell before that, and frankly the concurrency in Go feels similar, but is a definite downgrade due to the lack of STM.

You can implement channels and select using STM, so these don’t have to be in the standard library. And the contentious design choices like what happens when you close the channel twice can be your choice! And going from STM to managing mutexes is a definite downgrade in abstraction power.

The concurrency design in Haskell feels like true magic.

12h agoHN ↗

The concurrency design in Haskell is cool, though I gotta admit that I don't find it much fun to write.

It's not because the language is "hard". I remember when I first learned Haskell a million years ago I thought it was the coolest thing ever because I had never seen anyone work at that abstract of a level before, especially in a compiled language. I got to understand the theory well enough and I know how to write a program with it, but the entire language kind of feels slapped together to me. Every time I've written anything in Haskell, I feel like I have to do a million compiler extensions, or rely on third party libraries' liberal use of Template Haskell (e.g. Lens) to make the language feel anywhere near "modern".

Yes yes yes, I know this is a complaint about GHC, not "Haskell", but given that GHC is basically the only Haskell compiler that gets serious use I don't think it's weird to conflate the compiler and the language.

10h agoHN ↗

Template Haskell is actually pretty cool. Using it to generate lenses in a type is a perfectly fine use case (of course hand-writing lenses is just one line anyways). Running computation at compile time is really a great feature; people rave about comptime in Zig but of course Haskell has had it earlier.

10h agoHN ↗

I don't dispute the coolness of any given Haskell feature. Haskell does have a lot of really neat features, but that doesn't mean that the language is fun to use.

C++ also has a lot of really cool features but I also do not enjoy writing it, actually for similar reasons as Haskell (though I don't think Haskell is nearly as irritating as C++).

of course hand-writing lenses is just one line anyways

The lenses themselves aren't hard to write; I was referring to the annoying quirk of Haskell where records couldn't have the same field names. Lens has a nice helper macro `makeFields` so that you could more or less automatically have the generated lenses have the clashing names.

To be fair it actually always worked fine for me but it always felt janky until `DuplicateRecordFields` was released.

9h agoHN ↗

the entire language kind of feels slapped together to me

The slogan "avoid success at all costs" definitely is accurate for Haskell

12h agoHN ↗

> in terms of how things can end up happening in any thread

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

11h agoHN ↗

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

10h agoHN ↗

I'm curious; using hardware threads is M logical threads preemptively scheduled on N physical cores. In what way does this not satisfy the original criteria?

10h agoHN ↗

That's just not what is referred to as green threads.

9h agoHN ↗

Yes, in implementation they are not. I'm curious what the difference in subjective experience is.

7h agoHN ↗

Green threads don’t allocate stack frames and do not require such a massive context switch, which in turn allows for many more threads.

7h agoHN ↗

> Preemptive scheduling plus M:N mapping

AFAIK, in the current implementation, Java's virtual threads yields only when they block (cooperative). But the spec allows a JVM to implement them as preemptive.

1h agoHN ↗

Well, they do only yield on block and whatnot, but that's very much different from adding manual async/await keywords, I would say.

7h agoHN ↗

C# and Rust (via Tokio) both have M:N threading. They both use a work-stealing algorithm to map many tasks onto a finite thread pool. But you're correct that they are cooperative via async/await, not pre-emptive.

4h agoHN ↗

Very curious how much Go wins by this. How worse would typical Go programs run with pre-emption disabled?

4h agoHN ↗

I believe it’s more about robustness than performance in typical cases. Without pre-emption, there’s always a risk of one goroutine using disproportionate CPU time if it gets into an infinite (or just very long) loop without doing any IO.

2h agoHN ↗

You can try this yourself: GODEBUG=asyncpreemptoff=1

Also platforms like Wasm still do Mx1 scheduling without async preemption, where Gosched is required at places.

E.g.: my "transpiled" SQLite driver takes special care to make sure long running SQL queries (and the busy handler) can be canceled with contexts even on platforms without async preemption.

2h agoHN ↗

Haskell had preemptive M:N green-threading before Go was invented.

I believe Go didn't originally have it, and added it in 2020, 14 years after Haskell.

10h agoHN ↗

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.

9h agoHN ↗

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).

7h agoHN ↗

Since you’re talking about threads in the context of the BEAM, you might want to give it a deeper look. There are no threads there, at least not OS threads on the developer’s disposal.

9h agoHN ↗

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.

8h agoHN ↗

Slightly irrelevant but that's a part of my issue with Go. I personally feel the chances are slim that "familiar programming concepts" (i.e. as taught by most intro CS courses) are optimal by themselves. And I know it's an old thing, but the fact that Go was once adamantly against generics...

6h agoHN ↗

I agree with everything you're saying. I think it comes down to design philosophy. Erlang's ecosystem is well tuned for building fault tolerant systems and features concurrency heavily to solve for that. Go is for general purpose programming in big organizations with massive variance in developer experience that has concurrency as a first class concept for the ability to scale (among other things).

Of course, in some sense fault tolerance and scaling are two sides of the same coin. They're both measures of availability. They're just different approaches to that.

What Go achieves that Erlang doesn't is the ability to "pick up and play". What Erlang achieves that Go doesn't is a pathological commitment to the system whole never going down.

4h agoHN ↗

The Go team as a whole was not adamantly against generics though, as I recall. Rather, they were against implementations that would have bad overall implications for the language (especially its complexity, both in usage and in implementation).

Once a sufficiently good proposal was made, generics were adopted.

24m agoHN ↗

I personally feel the chances are slim that "familiar programming concepts" (i.e. as taught by most intro CS courses) are optimal by themselves.

On the other hand, Erlang has been out for ages and has largely failed to attract much adoption, so it doesn’t seem like the market finds it to be “optimal” either. Not that popularity is everything, but over time a language better languages should increase their market share, especially if your language got its start during an era where the competition was C and C++ and Java.

And I know it's an old thing, but the fact that Go was once adamantly against generics...

Erlang not only lacks generics, but it lacks any static type system at all…

7h agoHN ↗

Not to mention its a lot easier to learn Go concurrency over Elixirs whole ecosystem. Also Go has a job market while Elixir job market exclusively consists out of senior level job postings that get handed over from Elixir job hopper to another Elixir job hopper. There is barely any reason to learn Elixir except for being fascinated by it.

6h agoHN ↗

Comparing learning one language's concurrency model with learning another language's entire ecosystem is incommensurable.

Having learned both Go and Elixir, I found Elixir easier to learn and a lot more enjoyable to work with. I'm not alone in this opinion. According to Stack Overflow's 2025 "admired" languages, Elixir scored 65.9% compared with Go's 56.5%; Phoenix was the most admired web framework of 2025 at 79% and has held that spot for the past three years.

6h agoHN ↗

Well to use Elixirs concurrency model you kinda have to use the rest of the langs ecosystem and learn a shit ton more compared to familiar feeling langs like Go. For Go I dont have to learn its execution model, some VM specifics and whatnot.

6h agoHN ↗

Erlang/Elxir would be more popular if it compiled to native. BEAM VM is not that performant and programs also take more memory compared to Go.

4h agoHN ↗

Erlang is dynamically typed, so many of the performance costs are similar to a JS runtime and always fully deciding typing ahead of time is an undecidable problem.

In practice, this is why many such languages have JIT's (unless targeting a subset or an type-information enhanced superset like TS), there was a seminal OOPSLA paper in 1995 by Agesen and Hölsze (who worked on the JVM Hotspot compiler) that compared JIT's to AOT compilation in practice (the Agesen CPA algorithm isn't perfect but it's pretty good for the time and others have probed that it's an undecidable problem).

That said, they also had a historically bad performance story due to misjudgments in development direction, the interpreter was default and they twice tried to make "HPC JIT's", ie.. complex JIT's that tried to be "perfect" and focused on numerical code gains, they'd be good for optimizing a matrix kernel, yet fairly useless or even negative on more common code patterns.

OTP 24,25 and 26 took learnings from the JS runtimes and also added compiler hints (since they already had binary precompiled modules).

What still saves the OTP runtime is that many basic operations that would suck without a good performance story is handled by built-in functions, so like Python most practical programs works well enough even if the runtime is behind.

4h agoHN ↗

I mean, Java does it even more elegantly imo and if anything it is the most stereotypical oop programming language of all time.

6h agoHN ↗

  Hello, Mike.
  Hello, Joe.
  System working?
  Seems to be.
  Okay. Fine.
  Okay.
5h agoHN ↗

The elixir documentary doesn't quite have the same zest

6h agoHN ↗

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.

This is misleading. I had an old Dell computer in my garage hosting a php app that hit that level of uptime as well over a 9 month period. It was 100% so actually better.

Those uptime numbers only hold water when spread over many thousands to millions of users where you’re at large enough scale that you’re actually dealing with a meaningful volume of hardware failures.

6h agoHN ↗

Did you come from reddit? The switch in question is a backbone switch and those at the time served tens of millions of users.

How many do you serve?

Edit: either you are a troll or you have an affinity for hateposting on HN. Nothing positive have come from your comments.

4h agoHN ↗

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.

2h agoHN ↗

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.

43m agoHN ↗

If you've written a few GenServers, I'm not sure one would describe it as "magic" in quite the same way. I wouldn't anyway.

8h agoHN ↗

I don't think you understand the threading concurrency topic. Also memory safety is so far off base here, where's that coming from? Java does some stuff okay, but do you really want to defend the horrid JVM problems? Also why can't I have my memory back when it's not in use in tightly packed systems?

It's not great for everything and neither is Go. You can find a bit more context on that in some of the other threads.

1h agoHN ↗

Also memory safety is so far off base here, where's that coming from?

It's coming from Go. In presence of data races on interfaces, slices or maps your memory might get corrupted.

Also why can't I have my memory back when it's not in use in tightly packed systems?

You can. You have to either set your GC to be more aggressive or you need to utilize value types more.

1h agoHN ↗

defend the horrid JVM problems

Such as? It's one of the most widely used platform for backend services, basically almost all top 500 company has some business critical infrastructure running Java. It surely can't have "too horrid" problems..

7h agoHN ↗

Kotlin's coroutines basically had the same API as Go's. But also the ability to confine some coroutines' execution into certain threads (e.g. UI main thread).

Then they added structured concurrency, which roughly solves the same problem as Go's context, arguably more elegantly.

7h agoHN ↗

Kotlins coroutines are super super super underrated imo. I loved working with them so much before ai. Fuck ai.

4h agoHN ↗

Sorry my ignorance, but what does AI have to do with you not being able to use kotlins concurrency model?

4h agoHN ↗

Not really, it is not knowing enough programming languages and computing history.

3h agoHN ↗

I used to feel the same way, then I started writing Rust. I got tasks (goroutines) and channels which are largely the same as Go - except I never need to worry about race conditions or nil pointers and it's nearly impossible for LLMs to generate broken code (bad code, yes, broken code no).

I have tried but I honestly can't go back to Go now, it's so much harder

2h agoHN ↗

I mean, that's one area where the story is not as nice in rust. Afaik the core abstraction is a bit leaky to be usable with tokio and other implementations as well.

3h agoHN ↗

It appears to me as if Golang has implemented part of the actor model. As I recall, it was neither set up to transfer free-form messages between the actors nor for the actors to persist beyond the given task.

Erlang has had both for over 20 years; it has also had green threads for equally as long—something I don't know if Golang has. I'm sure Golang cannot split itself to run on multiple machines with its actor model. Erlang can.

1h agoHN ↗

I haven't found a Go concurrency thing yet that hasn't long-existed in Haskell before.

I also find Haskell's concurrency in practice much easier to reason about than Go's, let me do a pitch:

In Haskell you can just fork a thread and block till it's done. Threaded, "async" logic just looks like blocking serial code (but isn't blocking). I feel like in typical channel-based Go code I have to jump and scroll a lot in the code because of all the message-passing instead of block-scoped "blocking-style" variable use, and that this makes it hard to conclude whether the whole thing terminates or deadlocks.

In Haskell, channels are considered low-level concurrency primitives you should only use when you have no clean high-level primitives for it. This is because they are not "structured" concurrency: When you send something into a channel, it is gone out of your scope, and you now need to track in your brain where it is, and who should consume that thing in the right way ("message-passing").

For example, in Stolon, a high-availability Postgres orchestrator written in Go (https://github.com/sorintlab/stolon), I found the logic for failover with multiple channels and various timeouts very difficult to reason about when investigating failover bugs. I'm pretty sure that would read much easier in Haskell (see below how).

In Haskell, you can start 2, or N, things in parallel, and easily wait till they are done. You can invoke parallel `map` easily.

    results <- mapConcurrently f mylist

If f throws on any element, the whole map throws, and other threads get cancelled automatically as expected.

You can get bounded, steaming parallelism, easily.

You can set time limits to function calls writing

    timeout 1000 (myIoFunction ...)

You can cancel any thread or computation, at any time. The same timeout function can cancel blocking IO operations, such as reading from the terminal or sockets, without having pass around `Context` objects like in Go (which, if you forget it, just makes things hang or deadlock).

You wrap the 2 words "timeout 1000" around your function and done.

Concurrency _composes_ in Haskell. You can write

    res :: Maybe (Maybe a) < timeout a (timeout b (myIoFunction ...))

and the returned type tells you cleanly at which level the cancellation occured (no mixing into the same `error` type.

You can build trees of parallel operations that live and die together.

And there are no data races (because mutability is a very explicit thing), and I'm not even mentioning STM here (which allows you to do database-style transactions across variables) because that's already pointed out in another post.

As a composed example, in Haskell you can write:

    timeout 1000 (race (downloadUrl ...) (forever (putStrLn "Still loading ...")))

and that will do exactly what you think it should, with correct Ctrl+C cancellability, and good developer ergonomics.

If you enjoy concurrency, give Haskell a shot!

1h agoHN ↗

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.

1h agoHN ↗

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.

1h agoHN ↗

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?

26m agoHN ↗

It's true for almost all pthread implementations, not all. But when talking about asynchronous I/O runtimes and coroutines, you have more options. Systems like Tokio, or zio (the one I'm working on), give you a task handle, and when you call `cancel()` on the handle, it will cancel whatever async operation the task is currently running. And it does so reliably.

45m agoHN ↗

Julia has a very good threading story. Task based, M:N, a lot of schedulers, structured concurrency, distributed. Sanest atomics I’ve seen. All in the stdlib.

25m agoHN ↗

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.

19m agoHN ↗

Asking as an outsider to Go. How do you communicate between threads without a channel or mutex?

9h agoHN ↗

Go concurrency seems simple on the surface, but mastering select and proper error handling takes practice. Good to see this topic distilled.

9h agoHN ↗

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.

9h agoHN ↗

Arguably, Java's virtual (green) threads managed through structured concurrency and futures is a superior approach.

8h agoHN ↗

Depends on what you're solving for. Message passing is fine. That's all channels does.

7h agoHN ↗

superior approach

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

6h agoHN ↗

Superior in ergonomics - launching several async tasks and combining their results via futures is is much easier compared in Java compared to to Go's low-level, primitive way of doing things. No need to explicitly create channels and wait on them. Go doesn't expose Go-routines as a type and hence you are brow-beaten into laboriously using channels even when there is no real need to do so. I guess this could be all sorted out if the Go stdlib offered some convenient structured concurrency packges.

5h agoHN ↗

This is why sync.WaitGroup exists, but you have to manage the lifecycle explicitly, it’s not built into the language.

5h agoHN ↗

While stdlib doesn't handle this, there are libraries that can support your use case https://github.com/jizhuozhi/go-future

If you only need to launch work and wait for completion, Go 1.25 has sync.WaitGroup.Go -> wg.Go(f) -> by wg.Wait(). No channels like the page says.

3h agoHN ↗

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.

3h agoHN ↗

If you make use of a 30 years of library that does simple blocking IO and you call that library from a virtual thread you literally have non-blocking behavior - so your "didn't update it's entire ecosystem" is plain wrong. It's also just a Thread, so even consuming virtual threads by old libs is just fine.

Also, what 'party'? There is java, go, Haskell and erlang with anything similar. The majority of programming languages don't have such a feature so it's pretty questionable use of word to "be late".

8h agoHN ↗

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.

5h agoHN ↗

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.)?

5h agoHN ↗

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.

4h agoHN ↗

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.

4h agoHN ↗

What kind of patterns?

I've started using golang last year and I feel like I'm missing exactly this kind of experience with these patterns

3h agoHN ↗

Concurrency has nothing to do with performance and everything to do with your domain. If what you're modeling is concurrent, your code should accordingly be concurrent also.

2h agoHN ↗

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

4h agoHN ↗

Channels are basically messaging queues in Java.

39m agoHN ↗

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.

8h agoHN ↗

Go has a really good concurrency story. Its one of the best ones out there. Some langs have async/await (usually sucks) and some nothing att all (like php)

5h agoHN ↗

Is a Go channel equivalent to a Haskell tvar ?

4h agoHN ↗

Closer to mvar. Tvars support full transactional semantics.

1h agoHN ↗

A go channel is just a queue with a configurable amount of buffering. Buffering 0 is the most interesting as it creates a “rendezvous” channel which syncs the sender and the receiver.

A channel of size 1 is a bit like an mvar but with support for only take and put.

4h agoHN ↗

One thing I always found more work than I would expect is when you have a graph of operations, think a Makefile, but a bit dynamic. For this model completable futures and executors seem to work well (provided the graphs is smallish), but golang is (or perhaps before generics) just was difficult.

2h agoHN ↗

honestly the hard part of go concurrency was never starting goroutines, it's making cancellation and shutdown behave. nice to see context, races and diagnostics in one runnable place.