Readit News logoReadit News
anacrolix · 5 months ago
I've been using Go since 2011. One year less than the author. Channels are bad. No prioritization. No combining with other synchronisation primitives without extra goroutines. In Go, no way to select on a variable number of channels (without more goroutines). The poor type system doesn't let you improve abstractions. Basically anywhere I see a channel in most people's code particular in the public interface, I know it's going to be buggy. And I've seen so many bugs. Lots of abandoned projects are because they started with channels and never dug themselves out.

The lure to use channels is too strong for new users.

The nil and various strange shapes of channel methods aren't really a problem they're just hard for newbs.

Channels in Go should really only be used for signalling, and only if you intend to use a select. They can also act as reducers, fan out in certain cases. Very often in those cases you have a very specific buffer size, and you're still only using them to avoid adding extra goroutines and reverting to pure signalling.

hajile · 5 months ago
This is almost completely down to Go's type terrible system and is more proof that Google should have improved SML/CML (StandardML/ConcurrentML) implementations/libraries rather than create a new language. They'd have a simpler and more powerful language without all the weirdness they've added on (eg, generics being elegant and simple rather than a tacked-on abomination of syntax that Go has).
hesdeadjim · 5 months ago
Go user for ten years and I don’t know what happened, but this year I hit some internal threshold with the garbage type system, tedious data structures, and incessant error checking being 38% of the LoC. I’m hesitant to even admit what language I’m considering a full pivot to.
thesz · 5 months ago
As you mentioned "improvement of existing language," I'd like to mention that Haskell has green threads that most probably are lighter (stack size 1K) than goroutines (minimum stack size 2K).

Haskell also has software transactional memory where one can implement one's own channels (they are implemented [1]) and atomically synchronize between arbitrarily complex reading/sending patterns.

[1] https://hackage.haskell.org/package/stm-2.5.3.1/docs/Control...

In my not so humble opinion, Go is a library in Haskell from the very beginning.

politician · 5 months ago
One nit: reflect.Select supports a dynamic set of channels. Very few programs need it though, so a rough API isn’t a bad trade-off. In my entire experience with Go, I’ve needed it once, and it worked perfectly.
lanstin · 5 months ago
I almost always only use Channels as the data path between fixed sized pools of workers. At each point I can control if blocking or not, and my code uses all the (allocated) CPUs pretty evenly. Channels are excellent for this data flow design use case.

I have a little pain when I do a cli as the work appears during the run and it’s tricky to guarantee you exit when all the work is done and not before. Usually Ihave a sleep one second, wait for wait group, sleep one more second at the end of the CLI main. If my work doesn’t take minutes or hours to run, I generally don’t use Go.

jessekv · 5 months ago
__s · 5 months ago
I'm guilty of this too https://github.com/PeerDB-io/peerdb/blob/d36da8bb2f4f6c1c821...

The inner channel is a poor man's future. Came up with this to have lua runtimes be able to process in parallel while maintaining ordering (A B C in, results of A B C out)

lanstin · 5 months ago
I have a channel for my gRPC calls to send work to the static and lock free workers; I have a channel of channels to reuse the same channels as allocating 40k channels per second was a bit of CPU. Some days I am very pleased with this fix and some days I am ashamed of it.
sapiogram · 5 months ago
You joke, but this is not uncommon at all among channels purists, and is the inevitable result when they try to create any kind of concurrency abstractions using channels.

Ugh... I hope I never have to work with channels again.

eikenberry · 5 months ago
I've always thought a lot of it was due to how channels + goroutines were designed with CSP in mind, but how often do you see CSP used "in the wild"? Go channels are good for implementing CSP and can be good at similar patterns. Not that this is a big secret, if you watch all the concurrency pattern videos they made in Go's early days you get a good feeling for what they are good at. But I can only think of a handful of time I've seen those patterns in use. Though much of this is likely due to having so much of our code designed by mid-level developers because we don't value experience in this field.
t8sr · 5 months ago
When I did my 20% on Go at Google, about 10 years ago, we already had a semi-formal rule that channels must not appear in exported function signatures. It turns out that using CSP in any large, complex codebase is asking for trouble, and that this is true even about projects where members of the core Go team did the CSP.

If you take enough steps back and really think about it, the only synchronization primitive that exists is a futex (and maybe atomics). Everything else is an abstraction of some kind. If you're really determined, you can build anything out of anything. That doesn't mean it's always a good idea.

Looking back, I'd say channels are far superior to condition variables as a synchronized cross-thread communication mechanism - when I use them these days, it's mostly for that. Locks (mutexes) are really performant and easy to understand and generally better for mutual exclusion. (It's in the name!)

i_don_t_know · 5 months ago
> When I did my 20% on Go at Google, about 10 years ago, we already had a semi-formal rule that channels must not appear in exported function signatures.

That sounds reasonable. From what little Erlang/Elixir code I’ve seen, the sending and receiving of messages is also hidden as an implementation detail in modules. The public interface did not expose concurrency or synchronization to callers. You might use them under the hood to implement your functionality, but it’s of no concern to callers, and you’re free to change the implementation without impacting callers.

throwawaymaths · 5 months ago
AND because they're usually hidden as implementation detail, a consumer of your module can create simple mocks of your module (or you can provide one)
dfawcus · 5 months ago
How large do you deem to be large in this context?

I had success in using a CSP style, with channels in many function signatures in a ~25k line codebase.

It had ~15 major types of process, probably about 30 fixed instances overall in a fixed graph, plus a dynamic sub-graph of around 5 processes per 'requested action'. So those sub-graph elements were the only parts which had to deal with tear-down, and clean up.

There were then additionally some minor types of 'process' (i.e. goroutines) within many of those major types, but they were easier to reason about as they only communicated with that major element.

Multiple requested actions could be present, so there could be multiple sets of those 5 process groups connected, but they had a maximum lifetime of a few minutes.

I only ended up using explicit mutexes in two of the major types of process. Where they happened to make most sense, and hence reduced system complexity. There were about 45 instances of the 'go' keyword.

(Updated numbers, as I'd initially misremembered/miscounted the number of major processes)

hedora · 5 months ago
How many developers did that scale to? Code bases that I’ve seen that are written in that style are completely illegible. Once the structure of the 30 node graph falls out of the last developer’s head, it’s basically game over.

To debug stuff by reading the code, each message ends up having 30 potential destinations.

If a request involves N sequential calls, the control flow can be as bad as 30^N paths. Reading the bodies of the methods that are invoked generally doesn’t tell you which of those paths are wired up.

In some real world code I have seen, a complicated thing wires up the control flow, so recovering the graph from the source code is equivalent to the halting problem.

None of these problems apply to async/await because the compiler can statically figure out what’s being invoked, and IDE’s are generally as good at figuring that out as the compiler.

catern · 4 months ago
>If you take enough steps back and really think about it, the only synchronization primitive that exists is a futex (and maybe atomics). Everything else is an abstraction of some kind.

You're going to be surprised when you learn that futexes are an abstraction too, ultimately relying on this thing called "cache coherence".

And you'll be really surprised when you learn how cache coherence is implemented.

ChrisSD · 5 months ago
I think the two basic synchronisation primitives are atomics and thread parking. Atomics allow you to share data between two or more concurrently running threads whereas parking allows you to control which threads are running concurrently. Whatever low-level primitives the OS provides (such as futexes) is more an implementation detail.

I would tentatively make the claim that channels (in the abstract) are at heart an interface rather than a type of synchronisation per se. They can be implemented using Mutexes, pure atomics (if each message is a single integer) or any number of different ways.

Of course, any specific implementation of a channel will have trade-offs. Some more so than others.

im3w1l · 5 months ago
To me message passing is like it's own thing. It's the most natural way of thinking about information flow in a system consisting of physically separated parts.
LtWorf · 5 months ago
What you think is not very relevant if it doesn't match how CPUs work.
throwaway150 · 5 months ago
What is "20% on Go"? What is it 20% of?
darkr · 5 months ago
At least historically, google engineers had 20% of their time to spend on projects not related to their core role
NiloCK · 5 months ago
Google historically allowed employees to self-direct 20% of their working time (onto any google project I think).
ramon156 · 5 months ago
I assume this means "20% of my work on go" aka 1 out of 5 work days working on golang
thomashabets2 · 5 months ago
Unlike the author, I would actually say that Go is bad. This article illustrates my frustration with Go very well, on a meta level.

Go's design consistently at every turn chose the simplest (one might say "dumbest", but I don't mean it entirely derogatory) way to do something. It was the simplest most obvious choice made by a very competent engineer. But it was entirely made in isolation, not by a language design expert.

Go designs did not actually go out and research language design. It just went with the gut feel of the designers.

But that's just it, those rules are there for a reason. It's like the rules of airplane design: Every single rule was written in blood. You toss those rules out (or don't even research them) at your own, and your user's, peril.

Go's design reminds me of Brexit, and the famous "The people of this country have had enough of experts". And like with Brexit, it's easy to give a lame catch phrase, which seems convincing and makes people go "well what's the problem with that, keeping it simple?".

Explaining just what the problem is with this "design by catchphrase" is illustrated by the article. It needs ~100 paragraphs (a quick error prone scan was 86 plus sample code) to explain just why these choices leads to a darkened room with rakes sprinkled all over it.

And this article is just about Go channels!

Go could get a 100 articles like this written about it, covering various aspects of its design. They all have the same root cause: Go's designers had enough of experts, and it takes longer to explain why something leads to bad outcomes, than to just show the catchphrase level "look at the happy path. Look at it!".

I dislike Java more than I dislike Go. But at least Java was designed, and doesn't have this particular meta-problem. When Go was made we knew better than to design languages this way.

kbolino · 5 months ago
Go's designers were experts. They had extensive experience building programming languages and operating systems.

But they were working in a bit of a vacuum. Not only were they mostly addressing the internal needs of Google, which is a write-only shop as far as the rest of the software industry is concerned, they also didn't have broad experience across many languages, and instead had deep experience with a few languages.

emtel · 5 months ago
Rob Pike was definitely not a PL expert and I don’t think he would claim to be. You can read his often-posted critique of C++ here: https://commandcenter.blogspot.com/2012/06/less-is-exponenti...

In it, he seems to believe that the primary use of types in programming languages is to build hierarchies. He seems totally unfamiliar with ideas behind ML or haskell.

thomashabets2 · 5 months ago
I guess we're going into the definition of the word "expert".

I don't think the word encompasses "have done it several times before, but has not actually even looked at the state of the art".

If you're a good enough engineer, you can build anything you want. That doesn't make you an expert.

I have built many websites. I'm not a web site building expert. Not even remotely.

0x696C6961 · 5 months ago
The Brexit comparison doesn't hold water — Brexit is widely viewed as a failure, yet Go continues to gain popularity year after year. If Go were truly as bad as described, developers wouldn't consistently return to it for new projects, but clearly, they do. Its simplicity isn't a rejection of expertise; it's a practical choice that's proven itself effective in real-world scenarios.
tl · 5 months ago
This is optics versus reality. Its goal was to address shortcomings in C++ and Java. It has replaced neither at Google and its own creators were surprised it competed with python, mostly on the value of having an easier build and deploy process.
thomashabets2 · 5 months ago
I would say this is another thing that would take quite a while to flesh out. Not only is it hard to have this conversation in text-only on hackernews, but HN will also rate limit replies, so a conversation once started cannot continue here to actually allow the discussion participants to come to an understanding of what the they all mean. Discussion will just stop once HN tells a poster "you're posting too often".

Hopefully saving this comment will work.

Go, unlike Brexit, has pivoted to become the solution to something other than its stated target. So sure, Go is not a failure. It was intended to be a systems language to replace C++, but has instead pivoted to be a "cloud language", or a replacement for Python. I would say that it's been a failure as a systems language. Especially if one tries to create something portable.

I do think that its simplicity is the rejection of the idea that there are experts out there, and/or their relevance. It's not decisions based on knowledge and rejection, but of ignorance and "scoping out" of hard problems.

Another long article could be written about the clearly not thought through use of nil pointers, especially typed vs untyped nil pointers (if that's even the term) once nil pointers (poorly) interact with interfaces.

But no, I'm not comparing the outcome of Go with Brexit. Go pivoting away from its stated goals are not the same thing as Brexiteers claiming a win from being treated better than the EU in the recent tariffs. But I do stand by my point that the decision process seems similarly expert hostile.

Go is clearly a success. It's just such a depressingly sad lost opportunity, too.

nvarsj · 5 months ago
The creators thought that having 50% of your codebase be `if (err != nil) { ... }` was a good idea. And that channels somehow make sense in a world without pattern matching or generics. So yeah, it's a bizarrely idiosyncratic language - albeit with moments of brilliance (like structural typing).

I actually think Java is the better PL, but the worse runtime (in what world are 10s GC pauses ever acceptable). Java has an amazing standard library as well - Golang doesn't even have many basic data structures implemented. And the ones it does, like heap, are absolutely awful to use.

I really just view Golang nowadays as a nicer C with garbage collection, useful for building self contained portable binaries.

cempaka · 4 months ago
> I actually think Java is the better PL, but the worse runtime (in what world are 10s GC pauses ever acceptable).

This seems like a very odd/outdated criticism. 10s CMS full STW GCs are a thing of the past. There are low-latency GCs available for free in OpenJDK now with sub-millisecond pause times. Where I've seen the two runtimes compared (e.g. Coinbase used both langs in latency-sensitive exchange components) Java's has generally come out ahead.

thomashabets2 · 5 months ago
I think Java made many decisions that turned out to be bad only in retrospect. In Go we knew (well, experts knew) already that the choices were bad.

Java is a child of the 90s. My full rant at https://blog.habets.se/2022/08/Java-a-fractal-of-bad-experim... :-)

Mawr · 5 months ago
Your post is pure hot air. It would be helpful if you could provide concrete examples of aspects of Go that you consider badly designed and why.
int_19h · 5 months ago
The intersection of nil and interfaces is basically one giant counter-intuitive footgun.

Or how append() sometimes returns a new slice and sometimes it doesn't (so if you forget to assign the result, sometimes it works and sometimes it doesn't). Which is understandable if you think about it in terms of low-level primitives, but in Go this somehow became the standard way of managing a high-level list of items.

Or that whole iota thing.

chabska · 5 months ago
> Go could get a 100 articles like this written about it, covering various aspects of its design

Actually... https://100go.co/

jmyeet · 5 months ago
The biggest mistake I see people make with Go channels is prematurely optimizing their code by making channels buffered. This is almost always a mistake. It seems logical. You don't want your code to block.

In reality, you've just made your code unpredictable and there's a good chance you don't know what'll happen when your buffered channel fills up and your code then actually blocks. You may have a deadlock and not realize it.

So if the default position is unbuffered channels (which it should be), you then realize at some point that this is an inferior version of cooperative async/await.

Another general principle is you want to avoid writing multithreaded application code. If you're locking mutexes or starting threads, you're probably going to have a bad time. An awful lot of code fits the model of serving an RPC or HTTP request and, if you can, you want that code to be single-threaded (async/await is fine).

franticgecko3 · 5 months ago
>The biggest mistake I see people make with Go channels is prematurely optimizing their code by making channels buffered. This is almost always a mistake. It seems logical. You don't want your code to block.

Thank you. I've fixed a lot of bugs in code that assumes because a channel is buffered it is non-blocking. Channels are always blocking, because they have a fixed capacity; my favorite preemptive fault-finding exercise is to go through a codebase and set all channels to be unbuffered, lo-and-behold there's deadlocks everywhere.

If that is the biggest mistake, then the second biggest mistake is attempting to increase performance of an application by increasing channel sizes.

A channel is a pipe connecting two workers, if you make the pipe wider the workers do not process their work any faster, it makes them more tolerant of jitter and that's it. I cringe when I see a channel buffer with a size greater than ~100 - it's a a telltale sign of a misguided optimization or finger waving session. I've seen some channels sized at 100k for "performance" reasons, where the consumer is pushing out to the network, say 1ms for processing and network egress. Are you really expecting the consumer to block for 100 seconds, or did you just think bigger number = faster?

dfawcus · 5 months ago
Yup, most of my uses were unbuffered, or with small buffers (i.e. 3 slots or fewer), often just one slot.
sapiogram · 5 months ago
> So if the default position is unbuffered channels (which it should be), you then realize at some point that this is an inferior version of cooperative async/await.

I feel so validated by this comment.

noor_z · 5 months ago
It's very possible I'm just bad at Go but it seems to me that the result of trying to adhere to CSP in my own Go projects is the increasing use of dedicated lifecycle management channels like `shutdownChan`. Time will tell how burdensome this pattern proves to be but it's definitely not trivial to maintain now.
fireflash38 · 5 months ago
Is using a server context a bad idea? Though tbh using it for the cancelation is a shutdown channel in disguise hah.
sapiogram · 5 months ago
You're not bad at Go, literally everyone I know who has tried to do this has concluded it's a bad idea. Just stop using channels, there's a nice language hidden underneath the CSP cruft.
vrosas · 5 months ago
I've found the smartest go engineer in the room is usually the one NOT using channels.
anarki8 · 5 months ago
I find myself using channels in async Rust more than any other sync primitives. No more deadlock headaches. Easy to combine multiple channels in one state-keeping loop using combinators. And the dead goroutines problem described in the article doesn't exist in Rust.
tuetuopay · 5 months ago
This article has an eerie feeling now that async rust is production grade and widely used. I do use a lot the basic pattern of `loop { select! { ... } }` that manages its own state.

And compared to the article, there's no dead coroutine, and no shared state managed by the coroutine: seeing the `NewGame` function return a `*Game` to the managed struct, this is an invitation for dumb bugs. This would be downright impossible in Rust, and coerces you in an actual CSP pattern where the interaction with the shared state is only through channels. Add a channel for exit, another for bookeeping, and you're golden.

I often have a feeling that a lot of the complaints are self-inflicted Go problems. The author briefly touches on them with the special snowflakes that are the stdlib's types. Yes, genericity is one point where channels are different, but the syntax is another one. Why on earth is a `chan <- elem` syntax necessary over `chan.Send(elem)`? This would make non-blocking versions trivial to expose and discover for users (hello Rust's `.try_send()` methods).

Oh and related to the first example of "exiting when all players left", we also see the lack of proper API for go channels: you can't query if there still are producers for the channel because gc and pointers and shared channel objetc itself and yadda. Meanwhile in rust, producers are reference-counted and the channel automatically closed when there are no more producers. The native Go channels can't do that (granted, they could, with a wrapper and dedicated sender and receiver types).

j-krieger · 5 months ago
> I do use a lot the basic pattern of `loop { select! { ... } }` that manages its own state.

Care to show any example? I'm interested!

ninkendo · 5 months ago
Same. It’s a pattern I’m reaching for a lot, whenever I have multiple logical things that need to run concurrently. Generally:

- A struct that represents the mutable state I’m wrapping

- A start(self) method which moves self to a tokio task running a loop reading from an mpsc::Receiver<Command> channel, and returns a Handle object which is cloneable and contains the mpsc::Sender end

- The handle can be used to send commands/requests (including one shot channels for replies)

- When the last handle is dropped, the mpsc channel is dropped and the loop ends

It basically lets me think of each logical concurrent service as being like a tcp server that accepts requests. They can call each other by holding instances of the Handle type and awaiting calls (this can still deadlock if there’s a call cycle and the handling code isn’t put on a background task… in practice I’ve never made this mistake though)

Some day I’ll maybe start using an actor framework (like Axum/etc) which formalizes this a bit more, but for now just making these types manually is simple enough.

surajrmal · 5 months ago
The fact all goroutines are detached is the real problem imo. I find you can encounter many of the same problems in rust with overuse of detached tasks.
pornel · 5 months ago
Channels are only problematic if they're the only tool you have in your toolbox, and you end up using them where they don't belong.

BTW, you can create a deadlock equivalent with channels if you write "wait for A, reply with B" and "wait for B, send A" logic somewhere. It's the same problem as ordering of nested locks.

j-krieger · 5 months ago
I haven't yet used channels anywhere in Rust, but my frustration with async mutexes is growing stronger. Do you care to show any examples?
tcfhgj · 4 months ago
async mutexes?

> Contrary to popular belief, it is ok and often preferred to use the ordinary Mutex from the standard library in asynchronous code.

> The feature that the async mutex offers over the blocking mutex is the ability to keep it locked across an .await point.

https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html

ricardobeat · 5 months ago
Strange to go all this length without mentioning the approaches that solve the problem in that first example:

1. send a close message on the channel that stops the goroutine

2. use a Context instance - `ctx.Done()` returns a channel you can select on

Both are quite easy to grasp and implement.

sapiogram · 5 months ago
You've misunderstood the example. The `scores` channel aggregates scores from all players, you can't close it just because one player leaves.

I'd really, really recommend that you try writing the code, like the post encourages. It's so much harder than it looks, which neatly sums up my overall experience with Go channels.

politician · 5 months ago
It’s not entirely clear whether the author is describing a single or multiplayer game.

Among the errors in the multiplayer case is the lack of score attribution which isn’t a bug with channels as much as it’s using an int channel when you needed a struct channel.

ricardobeat · 5 months ago
In both examples, the HandlePlayer for loop only exits if .NextScore returns an error.

In both cases, you’d need to keep track of connected players to stop the game loop and teardown the Game instance. Closing the channel during that teardown is not a hurdle.

What am I missing?

Deleted Comment

jtolds · 5 months ago
Hi! No, I think you've misunderstood the assignment. The example posits that you have a "game" running, which should end when the last player leaves. While only using channels as a synchronization primitive (a la CSP), at what point do you decide the last player has left, and where and when do you call close on the channel?
taberiand · 5 months ago
I don't think there's much trouble at all fixing the toy example by extending the message type to allow communication of the additional conditions, and I think my changes are better than the alternative of using a mutex. Have I overlooked something?

Assuming the number of players are set up front, and players can only play or leave, not join. If the expectation is that players can come and go freely and the game ends some time after all players have left, I believe this pattern can still be used with minor adjustment

(please overlook the pseudo code adjustments, I'm writing on my phone - I believe this translates reasonably into compilable Go code):

  type Message struct {
    exit bool
    score    int
    reply chan bool
  }

  type Game struct {
    bestScore int
    players int // > 0
    messages    chan Message
  }

  func (g *Game) run() {
    for message := range g.messages {
      if message.exit {
        g.players = g.players - 1;

        if g.players == 0 {
          return
        }
        continue
      }
  
      if g.bestScore < 100 && g.bestScore < message.score {
        g.bestScore = message.score
      }

      acceptingScores := g.bestScore < 100

      message.reply <- acceptingScores
    }
  }

  func (g *Game) HandlePlayer(p Player) error {
    for {
      score, err := p.NextScore()
      if err != nil {
        g.messages <- { exit: true
      }
      return err
    }
    g.messages <- { score, reply }
    if not <- reply {
       g.messages <- { exit: true }
       return nil
      }
    }
  }

blablabla123 · 5 months ago
I don't think channels should be used for everything. In some cases I think it's possible to end up with very lean code. But yes, if you have a stop channel for the other stop channel it probably means you should build your code around other mechanisms.

Since CSP is mentioned, how much would this apply to most applications anyway? If I write a small server program, I probably won't want to write it on paper first. With one possible exception I never heard of anyone writing programs based on CSP (calculations?)

Deleted Comment

aflag · 5 months ago
Naive question, can't you just have a player count alongside the best score and leave when that reaches 0?
angra_mainyu · 5 months ago
Haven't read the article but it sounds like a waitgroup would suffice.
regularfry · 5 months ago
This was 2016. Is it all still true? I know things will be backwards compatible, but I haven't kept track of what else has made it into the toolbox since then.
sapiogram · 5 months ago
Absolutely nothing has changed at the language level, and for using channels and the `go` keyword directly, there isn't really tooling to help either.

Most experienced Golang practitioners have reached the same conclusions as this blog post: Just don't use channels, even for problems that look simple. I used Go professionally for two years, and it's by far the worst thing about the language. The number of footguns is astounding.

pdimitar · 4 months ago
Okay, but what do you use then?
fpoling · 5 months ago
The only thing that changed was Context and its support in networking and other libraries to do asynchronous cancellation. It made managing network connections with channels somewhat easier.

But in general the conclusion still stands. Channels brings unnecessarily complexity. In practice message passing with one queue per goroutine and support for priority message delivery (which one cannot implement with channels) gives better designs with less issues.

NBJack · 5 months ago
My hot take on context is that it's secretly an anti-pattern used only because of resistance to thread locals. While I understand the desire to avoid spooky action at a distance, the fact that I have to include it in every function signature I could possibly use it in is just a bit exhausting. Given I could inadvertently spin up a new one at will also makes me a bit uneasy.
athoscouto · 5 months ago
Yes. See update 2 FTA for a 2019 study on go concurrency bugs. Most go devs that I know consider using higher level synchronization mechanisms the right way to go (pun intended). sync.WaitGroup and errgroup are two common used options.
mort96 · 5 months ago
Channels haven't really changed since then, unless there was some significant evolution between 2016 and ~2018 that I don't know about. 2025 Go code that uses channels looks very similar to 2018 Go code that uses channels.
regularfry · 5 months ago
I'm also wondering about the internals though. There are a couple of places that GC and the hypothetical sufficiently-smart-compiler are called out in the article where you could think there might be improvements possible without breaking existing code.

Deleted Comment