[HN Gopher] Problems with Go channels (2016)
___________________________________________________________________
Problems with Go channels (2016)
Author : mpweiher
Score : 129 points
Date : 2025-04-13 05:43 UTC (17 hours ago)
(HTM) web link (www.jtolio.com)
(TXT) w3m dump (www.jtolio.com)
| t8sr wrote:
| 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!)
| throwaway150 wrote:
| What is "20% on Go"? What is it 20% of?
| NiloCK wrote:
| Google historically allowed employees to self-direct 20% of
| their working time (onto any google project I think).
| darkr wrote:
| At least historically, google engineers had 20% of their time
| to spend on projects not related to their core role
| kyrra wrote:
| This still exists today. For example, I am on the payments
| team but I have a 20% project working on protobuf. I had to
| get formal approval from my management chain and someone on
| the protobuf team. And it is tracked as part of my
| performance reviews. They just want to make sure I'm not
| building something useless that nobody wants and that I'm
| not just wasting the company's time.
| vrosas wrote:
| I see why they do this, but man it almost feels like
| asking your boss for approval on where you go on
| vacation. Do people get dinged if their 20% time project
| doesn't pan out, or they lose interest later on?
| NBJack wrote:
| Previously it could be anything you wanted. These days,
| you need formal approval. Google has changed a bit.
| kyrra wrote:
| It has nothing to do with success. It's entirely for
| making sure some one besides the person doing the 20%
| agrees with the idea behind the project.
| hedora wrote:
| Lol. They'd be better off giving people the option to
| work 4 days if they also signed over right of first
| refusal for hobby projects.
| rollcat wrote:
| I never worked at Google (or any other large corp for
| that matter), but this sounds like the exact opposite of
| an environment that spawned GMail.
|
| As you think back even to the very early days of
| computing, you'll find individuals or small teams like
| Grace Hopper, the Unix gang, PARC, etc that managed to
| change history by "building something useless". Granted,
| throughout history that happened less than 1% of the
| time, but it will never happen if you never try.
|
| Maybe Google no longer has any space for innovation.
| jasode wrote:
| _> I never worked at Google (or any other large corp for
| that matter), but this sounds like the exact opposite of
| an environment that spawned GMail._
|
| Friendly fyi... GMail was not a "20% project" which I
| mentioned previously:
| https://news.ycombinator.com/item?id=39052748
|
| Somebody (not me but maybe a Google employee) also
| revised the Wikipedia article a few hours after my
| comment: https://en.wikipedia.org/w/index.php?title=Side_
| project_time...
|
| Before LLMs and ChatGPT even existed ... a lot of us
| somehow hallucinated the idea that GMail came from
| Google's 20% Rule. E.g. from 2013-08-16 :
| https://news.ycombinator.com/item?id=6223466
| rollcat wrote:
| I see, thank you for debunking. But I think my general
| point still stands. You can progress by addressing a
| need, but true innovation requires adequate space.
| codr7 wrote:
| Which misses the point of 20% imo; exploring space that
| would likely be missed in business as usual, encouraging
| creativity.
| ramon156 wrote:
| I assume this means "20% of my work on go" aka 1 out of 5
| work days working on golang
| dfawcus wrote:
| 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 wrote:
| 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.
| dfawcus wrote:
| That was two main developers, one doing most of the code
| and design, the other a largely closed subset of 3 or 4
| nodes. Plus three other developers co-opted for
| implementing some of the nodes. [1]
|
| The problem space itself could have probably grown to twice
| the number of lines of code, but there wouldn't have needed
| to be any more developers. Possibly only the original two.
| The others were only added for meeting deadlines.
|
| As to the graph, it was fixed, but not a full mesh. A set
| of pipelines, with no power of N issue, as the collection
| of places things could talk to was fixed.
|
| A simple diagram represented the major message flow between
| those 30 nodes.
|
| Testing of each node was able to be performed in isolation,
| so UT of each node covered most of the behaviour. The bugs
| were three deadlocks, one between two major nodes, one with
| one major node.
|
| The logging around the trigger for the deadlock allowed the
| cause to be determined and fixed. The bugs arose due to
| time constraints having prevented an analysis of the
| message flows to detect the loops/locks.
|
| So for most messages, there were a limited number of
| destinations, mostly two, for some 5.
|
| For a given "request", the flow of messages to the end of
| the fixed graph would be passing through 3 major nodes.
| That then spawned the creation of the dynamic graph, with
| it having two major flows. One a control flow through
| another 3, the other a data flow through a different 3.
|
| Within that dynamic graph there was a richer flow of
| messages, but the external flow from it simply had the two
| major paths.
|
| Yes, reading the bodies of the methods does not inform as
| to the flows. One either had to read the "main" routine
| which built the graph, or better refer to the graph diagram
| and message flows in the design document.
|
| Essentially a similar problem to dealing with
| "microservices", or plugable call-backs, where the
| structure can not easily be determined from the code alone.
| This is where design documentation is necessary.
|
| However I found it easier to comprehend and work with /
| debug due to each node being a prodable "black box", plus
| having the graph of connections and message flows.
|
| [1] Of those, only the first had any exerience with CSP or
| Go. The CSP expereince being with a library for C, the Go
| experience some minimal use a year earlier. The other
| developers were all new to CSP and Go. The first two
| developers were "senior" / "experienced".
| ChrisSD wrote:
| 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 wrote:
| 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 wrote:
| What you think is not very relevant if it doesn't match how
| CPUs work.
| ChrisSD wrote:
| huh?
| hedora wrote:
| I think they mean that message channels are an expensive
| and performance unstable abstraction.
|
| You could address the concern by choosing a CPU
| architecture that included infinite capacity FIFOS that
| connected its cores into arbitrary runtime directed
| graphs.
|
| Of course, that architecture doesn't exist. If it did,
| dispatching an instruction would have infinite tail
| latency and unbounded power consumption.
| i_don_t_know wrote:
| > 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 wrote:
| 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)
| ricardobeat wrote:
| 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.
| jtolds wrote:
| 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?
| blablabla123 wrote:
| 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?)
| franticgecko3 wrote:
| > 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?)
|
| CSP is really in the realm of formal methods. No you
| wouldn't formulate your server program as CSP, but if you
| were writing software for a medical device, perhaps.
|
| This is the FDR4 model checker for CSP, it's a functional
| programming language that implements CSP semantics and may
| be used to assert (by exhaustion, IIRC) the correctness of
| your CSP model.
|
| https://cocotec.io/fdr/
|
| I believe I'm in the minority of Go developers that have
| studied CSP, I fell into Go by accident and only took a CSP
| course at university because it was interesting, however I
| do give credit to studying CSP for my successes with Go.
| aflag wrote:
| Naive question, can't you just have a player count alongside
| the best score and leave when that reaches 0?
| jtolds wrote:
| Adding an atomic counter is absolutely a great solution in
| the real world, definitely, and compare and swap or a mutex
| or similar totally is what you want to do. In fact, that's
| my point in that part of the post - you want an atomic
| variable or a mutex or something there. Other
| synchronization primitives are more useful than sticking
| with the CSP idea of only using channels for
| synchronization.
| angra_mainyu wrote:
| Haven't read the article but it sounds like a waitgroup would
| suffice.
| taberiand wrote:
| 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 } } }
| sapiogram wrote:
| 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 wrote:
| 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.
| jeremyjh wrote:
| The whole point is that it is multiplayer.
| ricardobeat wrote:
| 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?
| guilhas wrote:
| I think nothing
|
| I was thinking the same, the only problem is the author not
| keeping track of players
|
| On HandlePlayer return err you would decrement a g.players
| counter, or something, and in the Game.run just do if
| !g.hasPlayers() break close(g.scores)
|
| The solution requires nothing special, just basic logic
| that should probably be there anyway
|
| If anything this post shows that mutexes are worse, by
| making bad code work
| regularfry wrote:
| 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.
| mort96 wrote:
| 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 wrote:
| 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.
| sapiogram wrote:
| 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.
| athoscouto wrote:
| 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.
| fpoling wrote:
| 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 wrote:
| 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.
| fpoling wrote:
| One of the often mentioned advantages of Go thread model is
| that it does not color functions allowing any code to start
| a goroutine. But with Context needed for any code that can
| block that advantage is lost with ctx argument being the
| color.
| Mawr wrote:
| Function arguments do not color functions. If you'd like
| to call a function that takes a Context argument without
| it, just pass in a context.Background(). It's a non-
| issue.
|
| Full rebuttal by jerf here:
| https://news.ycombinator.com/item?id=39317648
| eptcyka wrote:
| Coloring is a non-issue, context itself is. Newcomers
| find it unintuitive, and implementing a cancellable piece
| of work is incredibly cumbersome. One can only cancel a
| read/write to a channel, implementing cancellation
| implies a 3 line increase to every channel operation,
| cancelling blocking I/O defers to the catchphrase that
| utilises the name of the language itself.
| fpoling wrote:
| Cancelling blocking IO is such a pain point in Go indeed.
| One can do it via careful Close() calls from another
| goroutine while ensuring that close is called only once.
| But Go provides absolutely no help for that in the
| standard library.
| dfawcus wrote:
| Context is not need for any code which can block.
|
| Context is only needed where one has a dynamic graph
| where one wishes to cleanly tear down that graph. One can
| operate blocking tx/rx within a fixed graph without the
| need for any Context to be present.
|
| Possibly folks are thinking of this only for "web
| services" type of things, where everying is being
| exchanged over an HTTP/HTTPS request.
|
| However channels / CSP can be used in much wider problem
| realms.
|
| The real gain I found from the CSP / Actor model approach
| to things is the ability to reason through the problems,
| and the ability to reason through the bugs.
|
| What I found with CSP is that I accept the possibility of
| accidentally introducing potential deadlocks (where there
| is a dependancy loop in messaging) but gain the ability
| to reason about state / data evolution. As opposed to the
| "multithreaded" access to manipulate data (albeit with
| locks/mutexes) where one can not obviously reason through
| how a change occurred, and its timing.
|
| For the bugs which occur in the two approaches, I found
| the deadlock with incorrect CSP to be a lot easier to
| debug (and fix) vs the unknown calling thread which
| happened to manipulate a piece of (mutex protected)
| shared state.
|
| This arises from the Actor like approach of a data change
| only occurring in the context of the one thread.
| fpoling wrote:
| Actor model does not need CSP. Erlang uses single message
| passing queue per thread (process in Erlang terminology)
| with unbounded buffer so the sender never blocks.
|
| As the article pointed out lack of support for unbounded
| channels complicates reasoning about absence of
| deadlocks. For example, they are not possible at all with
| Erlang model.
|
| Of cause unbounded message queues have own drawbacks, but
| then Erlang supports safe killing of threads from other
| threads so with Erlang a supervisor thread can send
| health pings periodically and kill the unresponsive
| thread.
| anarki8 wrote:
| 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.
| ninkendo wrote:
| 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.
| tuetuopay wrote:
| 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 wrote:
| > I do use a lot the basic pattern of `loop { select! { ... }
| }` that manages its own state.
|
| Care to show any example? I'm interested!
| j-krieger wrote:
| 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?
| pornel wrote:
| 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.
| surajrmal wrote:
| 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.
| noor_z wrote:
| 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.
| sapiogram wrote:
| 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 wrote:
| I've found the smartest go engineer in the room is usually
| the one NOT using channels.
| fireflash38 wrote:
| Is using a server context a bad idea? Though tbh using it for
| the cancelation is a shutdown channel in disguise hah.
| fpoling wrote:
| I once read a book from 1982 that was arguing about CSP
| implementation in Ada that it lead to proliferation of threads
| (called tasks in Ada) and code complexity when mutex -based
| solutions were simpler.
|
| Go implementations of CSP somewhat mitigated the problems raised
| in the book by supporting buffered channels, but even with that
| with CSP one end up with unnecessary tasks which also brings the
| problem of their lifetime management as the article mentioned.
| sapiogram wrote:
| Unfortunately, Go also made their channels _worse_ by having
| their nil semantics be complete lunacy. See the "channel API
| is inconsistent and just cray-cray" section in the article.
| chuckadams wrote:
| The fact that Go requires a zero value for everything,
| including nil for objects like it was Java and the 1990's all
| over again, is one of the reasons I'm not particularly
| inclined to use Go for anything. Even PHP is non-nullable-by-
| default nowadays.
| NBJack wrote:
| Wait until you try refactoring in Go. Java has plenty of
| warts, but the explicit inheritance in it and other
| languages makes working across large codebases much simpler
| when you need to restructure something. Structural typing
| is surprisingly messy in practice.
| chuckadams wrote:
| I love structural typing, use it all day long in
| TypeScript, and wish every language had it. I've never
| run into issues with refactoring, though I suppose
| renaming a field in a type when the consumers are only
| matching by a literal shape might be less than 100%
| reliable. It looks like Go does support anonymous
| interfaces, but I'm not aware of how much they're
| actually used in real-world code (whereas in TS inlined
| object shapes are fairly common in trivial cases).
| kbolino wrote:
| I too like it, in fact I find myself missing it in other
| languages. I will say that anonymous interfaces are kind
| of rare in Go, and more generally interfaces in my
| experience as used in real codebases seem to lean more on
| the side of being producer-defined rather than consumer-
| defined.
|
| But there are a couple of problems I can think of.
|
| The first is over-scoping: a structural interface is
| matched by everything that appears to implement it. This
| can complicate refactoring when you just want to focus on
| those types that implement a "specialized" form of the
| interface. So, the standard library's Stringer interface
| (with a single `String() string` method) is
| indistinguishable from my codebase's MyStringer interface
| with the exact same method. A type can't say it
| implements MyStringer but not Stringer. The solution for
| this is dummy methods (add another method `MyStringer()`
| that does nothing) and/or mangled names (change the only
| method to `MyString() string` instead of `String()
| string`) but you do have to plan ahead for this.
|
| The second is under-matching: you might have intended to
| implement an interface only to realize too late that you
| didn't match its signature exactly. Now you may have a
| bunch of types that can't be found as implementations of
| your interface even though they were meant to be. If you
| had explicit interface implementation, this wouldn't be a
| problem, as the compiler would have complained early on.
| However, this too has a solution: you can statically
| assert an interface is supposed to be implemented with
| `var _ InterfaceType = ImplementingType{}` (with the
| right-hand side adjusted as needed to match the type in
| question).
| kbolino wrote:
| Nil is IMO far and away Go's biggest wart, especially today
| with a lot of the early language's other warts filed off.
| And there's really no easy way to fix this one.
|
| I think this article on channels suffers from "Seinfeld is
| Unfunny" syndrome, because the complaints about channels
| have largely been received and agreed upon by experienced
| Go developers. Channels are still useful but they were way
| more prominent in the early days of Go as a solution to
| lots of problems, and nowadays are instead understood as a
| sharp tool only useful for specific problems. There's
| plenty of other tools in the toolbox, so it was easy to
| move on.
|
| Whereas, nil is still a pain in the ass. Have any
| nontrivial data that needs to turn into or come from JSON
| (or SQL, or XML, or ...)? Chances are good you'll need
| pointers to represent optionality. Chaining
| structVal.Foo.Bar.Qux is a panic-ridden nightmare, while
| checking each layer results in a massive amount of
| boilerplate code that you have to write again and again.
| Heck, you might even need to handle nil slices specially
| because the other side considers "null" and "[]"
| meaningfully distinct! At least nil slices are safe in most
| places, while nil maps are panic-prone.
| jtolds wrote:
| > the complaints about channels have largely been
| received and agreed upon by experienced Go developers.
| Channels are still useful but they were way more
| prominent in the early days of Go as a solution to lots
| of problems, and nowadays are instead understood as a
| sharp tool only useful for specific problems.
|
| As the author of the post, it's really gratifying to hear
| that this is your assessment nowadays. I agree, and while
| I'm not sure I had much to do with this turn of events
| (it probably would have happened with or without me),
| curbing the use of channels is precisely why I wrote the
| post. I felt like Go could be much better if everyone
| stopped messing around with them. So, hooray!
| anacrolix wrote:
| 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.
| politician wrote:
| 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 wrote:
| 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 wrote:
| How about channel channels?
|
| https://github.com/twpayne/go-pubsub/blob/master/pubsub.go#L...
| sapiogram wrote:
| 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.
| __s wrote:
| 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 wrote:
| 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.
| hajile wrote:
| 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 wrote:
| 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.
| chuckadams wrote:
| Raku? Unison? Qi? Don't tell me it's something boring like
| C# ;)
| hesdeadjim wrote:
| Caught me, C#. Library quality has improved a lot in ten
| years, the language feels modern, and one of Go's biggest
| advantages, single binary cross-compile, is way less
| relevant now that dotnet standard installs easily on
| every OS I care about. I was prototyping some code that
| needed to talk to OpenAI, Slack, and Linear and the
| result was.. fast and extremely readable inline async
| code. I've interacted with these APIs in Go as well and
| by comparison, ultra clunky.
|
| We're a video game studio as well using C#, and while
| game programmer != backend programmer, I can at least
| delegate small fixes and enhancements out to the team
| more easily.
| hedora wrote:
| "Game studio" suggests you made the right choice, but the
| advantages you mention apply to rust and typescript too.
| Both those alternatives are data race free, unlike go, c#
| c++ and java. (Typescript is single threaded and gc'ed.
| Together, those properties mean it doesn't need a borrow
| checker.)
| likeabbas wrote:
| Java 21 is pretty damn nice, 25 will be even nicer.
|
| For your own application code, you don't have to use
| exceptions you can write custom Result objects and force
| callers to pattern match on the types (and you can always
| wrap library/std exceptions in that result type).
|
| Structured Concurrency looks like a _banger_ of a feature -
| it 's what CompletableFuture should've been.
|
| VirtualThreads still needs a few more years for most
| production cases imo, but once it's there, I truly don't
| see a point to choose Go over Java for backend web
| services.
| fpoling wrote:
| And Java has non-trivial advantage over Go of being arch-
| independent. So one can just run and debug on Mac Arm the
| same deployment artifact that runs on x86 server.
|
| Plus these days Java GC has addressed most of the
| problems that plagued Java on backend for years. The
| memory usage is still higher than with Go simply because
| more dynamic allocations happens due to the nature of the
| language, but GC pauses are no longer a significant
| problem. And if they do, switching to Go would not help.
| One needs non-GC language then.
| synergy20 wrote:
| go can ship as a static exe, can't be simpler to deploy,
| until java has this built-in, I'll stick with go for my
| cross platform choice
| t-writescode wrote:
| And if you want even cleaner and simpler syntax, while
| getting the benefits of the JVM, then Kotlin is a nice
| "step up" from there!
| j-krieger wrote:
| I like the idea behind Go, but I feel physical pain
| everytime I have some sort of `go mod` behaviour that is
| not immediately obvious. Import/Export is so easy, I still
| don't get how you can fuck it up.
| myaccountonhn wrote:
| OCaml is underrated IMO. It's a systems language like Go
| with a simple runtime but functional with a great type
| system and probably best error handling out of any language
| I've used (polymorphic variants).
| treyd wrote:
| OCaml and Go are _not_ considered systems programming
| languages, in part due to having required heavy runtimes
| and garbage collection.
| eikenberry wrote:
| 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.
| zerr wrote:
| Is the tl;dr: instead of channels, just use mutexes (and a shared
| state) explicitly with goroutines?
| guilhas wrote:
| Use the one that fits your problem
| https://go.dev/wiki/MutexOrChannel
|
| But in any case you will end up using a wrapper on either
| politician wrote:
| My rule of thumb is that the goroutine that writes to a channel
| is responsible for closing it. In this case, a deferred call to
| close the channel in HandlePlayer is sufficient.
|
| Still, this example has other issues (naked range over a
| channel?!) potentially contributing to the author's confusion.
|
| However, this post was also written almost a decade ago, so
| perhaps it's a result of being new to the language? If I cared to
| look, I'd probably be able to find the corresponding HN thread
| from that year full of arguments about this, hah.
| sateesh wrote:
| > In this case, a deferred call to close the channel in
| HandlePlayer is sufficient
|
| It is not clear from the example, but I presume there would
| multiple players, i.e there will calls of the form:
| g.HandlePlayer(p1) g.HandlePlayer(p2) ..
|
| in such a case one player closing the channel would affect rest
| of the producers too.
| sapiogram wrote:
| > My rule of thumb is that the goroutine that writes to a
| channel is responsible for closing it. In this case, a deferred
| call to close the channel in HandlePlayer is sufficient.
|
| This isn't your rule of thumb, it's the only practical way to
| do it. The problems arise when you have multiple goroutines
| writing to a channel, which is the case here.
|
| > Still, this example has other issues (naked range over a
| channel?!) potentially contributing to the author's confusion.
|
| You sound way more confused than the author. I think you've
| misunderstood what the (admittedly very abstract) example is
| supposed to be doing.
| franticgecko3 wrote:
| I'd like to refute the 'channels are slow' part of this article.
|
| If you run a microbenchmark which seems like what has been done,
| then channels look slow.
|
| If you try the contention with thousands of goroutines on a high
| core count machine, there is a significant inflection point where
| channels start outperforming sync.Mutex
|
| The reason is that sync.Mutex, if left to wait long enough will
| enter a slow code path and if memory serves, will call out to a
| kernel futex. The channel will not do this because the mutex that
| a channel is built with is exists in the go runtime - that's the
| special sauce the author is complaining doesn't exist but didn't
| try hard enough to seek it out.
|
| Anecdotally, we have ~2m lines of Go and use channels extensively
| in a message passing style. We do not use channels to increment a
| shared number, because that's ridiculous and the author is
| disingenuous in their contrived example. No serious Go shop is
| using a channel for that.
| chuckadams wrote:
| > We do not use channels to increment a shared number, because
| that's ridiculous and the author is disingenuous in their
| contrived example. No serious Go shop is using a channel for
| that.
|
| Talk about knocking down strawmen: it's a stand-in for shared
| state, and understanding that should be a minimum bar for
| serious discussion.
| franticgecko3 wrote:
| And implying I don't understand toy examples and responding
| with this is apparently above the bar for serious discussion.
| mrkeen wrote:
| According to the article, channels are slow because they use
| mutexes under the hood. So it doesn't follow that channels are
| better than mutexes for large N. Or is the article wrong? Or my
| reasoning?
| franticgecko3 wrote:
| I have replied to another comment with more details: the
| channel mutex is not the same one that sync.Mutex is using.
|
| The article that the OP article references does not show the
| code for their benchmark, but I must assume it's not using a
| large number of goroutines.
| n_u wrote:
| Do you have any benchmarks for the pattern you described where
| channels are more efficient?
|
| > sync.Mutex, if left to wait long enough will enter a slow
| code path and if memory serves, will call out to a kernel
| futex. The channel will not do this because the mutex that a
| channel is built with is exists in the go runtime
|
| Do you have any more details about this? Why isn't sync.Mutex
| implemented with that same mutex channels use?
|
| > [we] use channels extensively in a message passing style. We
| do not use channels to increment a shared number
|
| What is the rule of thumb your Go shop uses for when to use
| channels vs mutexes?
| franticgecko3 wrote:
| > Do you have any benchmarks for the pattern you described
| where channels are more efficient?
|
| https://go.dev/play/p/qXwMJoKxylT
|
| go test -bench=.* -run=^$ -benchtime=1x
|
| Since my critique of the OP is that it's a contrived example,
| I should mention so is this: the mutex version should be a
| sync.Atomic and the channel version should have one channel
| per goroutine if you were attempting to write a performant
| concurrent counter, both of those alternatives would have low
| or zero lock contention. In production code, I would be using
| sync.Atomic, of course.
|
| On my 8c16t machine, the inflection point is around 2^14
| goroutines - after which the mutex version becomes
| drastically slower; this is where I believe it starts
| frequently entering `lockSlow`. I encourage you to run this
| for yourself.
|
| > Do you have any more details about this? Why isn't
| sync.Mutex implemented with that same mutex channels use?
|
| Why? Designing and implementing concurrent runtimes has not
| made its way onto my CV yet; hopefully a lurking Go
| contributor can comment.
|
| The channel mutex: https://go.dev/src/runtime/chan.go
|
| Is not the same mutex as a sync.Mutex:
| https://go.dev/src/internal/sync/mutex.go
|
| If I had to guess, the channel mutex may be specialised since
| it protects only enqueuing or dequeuing onto a simple buffer.
| A sync.Mutex is a general construct that can protect any kind
| of critical region.
|
| > What is the rule of thumb your Go shop uses for when to use
| channels vs mutexes?
|
| Rule of thumb: if it feels like a Kafka use case but within
| the bounds of the local program, it's probably a good bet.
|
| If the communication pattern is passing streams of work where
| goroutines have an acyclic communication dependency graph,
| then it's a no brainer: channels will be performant and a
| deadlock will be hard to introduce.
|
| If you are using channels to protect shared memory, and you
| can squint and see a badly implemented Mutex or WaitGroup or
| Atomic; then you shouldn't be using channels.
|
| Channels shine where goroutines are just pulling new work
| from a stream of work items. At least in my line of work,
| that is about 80% of the cases where a synchronization
| primitive is used.
| n_u wrote:
| Thanks for the example! I'll play around with it.
|
| > On my machine, the inflection point is around 10^14
| goroutines - after which the mutex version becomes
| drastically slower;
|
| How often are you reaching 10^14 goroutines accessing a
| shared resource on a single process in production? We
| mostly use short-lived small AWS spot instances so I never
| see anything like that.
|
| > Why? Designing and implementing concurrent runtimes has
| not made its way onto my CV yet; hopefully a lurking Go
| contributor can comment. > If I had to guess, the channel
| mutex may be specialised since it protects only enqueuing
| or dequeuing onto a simple buffer. A sync.Mutex is a
| general construct that can protect any kind of critical
| region.
|
| Haha fair enough, I also know little about mutex
| implementation details. Optimized specialized tool vs
| generic tool feels like a reasonable first guess.
|
| Though I wonder if you are able to use channels for more
| generic mutex purposes is it less efficient in those cases?
| I guess I'll have to do some benchmarking myself.
|
| > If the communication pattern is passing streams of work
| where goroutines have an acyclic communication dependency
| graph, then it's a no brainer: channels will be performant
| and a deadlock will be hard to introduce.
|
| I agree with your rules, I used to always use channels for
| single processt thread-safe queues (similar to your Kafka
| rule) but recently I ran into a cyclic communication
| pattern with a queue and eventually relented to using a
| Mutex. I wonder if there are other painful channel
| concurrency patterns lurking for me to waste time on.
| franticgecko3 wrote:
| > How often are you reaching 10^14 goroutines accessing a
| shared resource on a single process in production? We
| mostly use short-lived small AWS spot instances so I
| never see anything like that.
|
| I apologize, that should've said 2^14, each sub-benchmark
| is a doubling of goroutines.
|
| 2^14 is 16000, which for contention of a shared resource
| is quite a reasonable order of magnitude.
| DeathArrow wrote:
| I don't know Go, but can't the situation be improved somehow?
| Either make channels better or remove them altogether?
| sapiogram wrote:
| As a language user? Just stop using them entirely. It's what
| most people do eventually.
| thomashabets2 wrote:
| 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 wrote:
| 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 wrote:
| 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.
| kbolino wrote:
| Rob Pike is not a PL theoretician, but that doesn't make
| him not an expert in creating programming languages.
|
| Go was the third language he played a major part in
| creating (predecessors are Newsqueak and Limbo), and his
| pedigree before Google includes extensive experience on
| Unix at Bell Labs. He didn't create C but he worked
| directly with the people who did and he likely knows it in
| and out. So I stand by my "deep, not broad" observation.
|
| Ken Thompson requires no introduction, though I don't think
| he was involved much beyond Go's internal development.
| Robert Griesemer is a little more obscure, but Go wasn't
| his first language either.
| elzbardico wrote:
| My point of view is that Rob Pike is a brilliant engineer,
| but a little too much opinionated for my tastes.
| thomashabets2 wrote:
| 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.
| kbolino wrote:
| I think both C and Go (the former is relevant due to
| Thompson's involvement in both and the massive influence it
| had on Go) are very "practical" languages, with strict
| goals in mind, and which delivered on those goals very
| well. They also couldn't have existed without battle-tested
| prior experience, including B for C and Limbo for Go.
|
| I also think it's only from the perspective of a select
| few, plus some purists, that the authors of Go can be
| considered anything other than experts. That they made some
| mistakes, including some borne of hubris, doesn't really
| diminish their expertise to me.
| thomashabets2 wrote:
| But my point is that articles like this show how that if
| you don't keep up with the state of the art, you run the
| risk of making this predictable mistakes.
|
| If Go had been designed in the 1980s then it would have
| been genius. But now we know better. Expertise is more
| than knowing state of the art as of 30 years prior.
| 0x696C6961 wrote:
| 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 wrote:
| 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.
| 0x696C6961 wrote:
| Go has replaced Java and C++ in numerous other
| environments.
| cyberpunk wrote:
| We use a boatload of off the shelf go components; but i
| don't see it making any progress at replacing java at my
| bank. We are extremely happy with where java is these
| days...
| dilyevsky wrote:
| > It has replaced neither
|
| Except that it did. Just because people aren't rewriting
| borg and spanner in go doesn't mean it isnt default choice
| for many of infra projects. And python got completely
| superseded by go even during my tenure
| thomashabets2 wrote:
| 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.
| 0x696C6961 wrote:
| > 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.
|
| Ok, I'll ask the obvious question. Who are these experts
| and what languages have they designed?
|
| > 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.
|
| You're getting worked up about something that's hardly ever
| an issue in practice. I suspect that most of your
| criticisms are similar.
| Mawr wrote:
| 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.
| nvarsj wrote:
| 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.
| guilhas wrote:
| Go channels, good or bad, are clearly a step up in
| concurrency/parallelism concepts and discourse
|
| I think that was one of the successes of Go
|
| Every big enough concurrent system will conclude sync primitives
| are dangerous and implement a queue system more similar to
| channels
|
| Mutexes always look easier for starters, but channels/queues will
| help you model the problem better in the long term, and debug
|
| Also as a rule of thumb you should probably handle panics every
| time you start a new thread/goroutine
| jmyeet wrote:
| 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).
| sapiogram wrote:
| > 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.
| franticgecko3 wrote:
| >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 wrote:
| Yup, most of my uses were unbuffered, or with small buffers
| (i.e. 3 slots or fewer), often just one slot.
| liendolucas wrote:
| Putting aside this particular topic, I'm seeing posts talking
| negatively about the language. I got my feet wet with Go many
| many years ago and for unknown reasons I never kept digging on
| it, so...
|
| Is it worth learning it? What problems are best solved with it?
| jtolds wrote:
| Author of the post here, I really like Go! It's my favorite
| language! It has absolutely nailed high concurrency programming
| in a way that other languages' solutions make me cringe to
| think through (await/async are so gross and unnecessary!)
|
| If you are intending to do something that has multiple
| concurrent tasks ongoing at the same time, I would definitely
| reach for Go (and maybe be very careful or skip entirely using
| channels). I also would reach for Go if you intend to work with
| a large group of other software engineers. Go is rigid; when I
| first started programming I thought I wanted maximum
| flexibility, but Go brings uniformity to a group of engineers'
| output in a way that makes the overall team much more
| productive IMO.
|
| Basically, I think Go is the best choice for server-side or
| backend programming, with an even stronger case when you're
| working with a team.
| liendolucas wrote:
| Thanks for the tip! Will definitely take into account your
| insights on channels if I decide to dive into it.
| gwd wrote:
| He mentions the lack of generics; I wonder how he'd make his own
| channel++ library now that generics are well-established.
| ajankovic wrote:
| I had time to spare so I toyed with the example exercise. Now I
| am not sure if I misunderstood something because solution is
| fairly simple using only channels:
| https://go.dev/play/p/tD8cWdKfkKW
| sateesh wrote:
| Your example solution has only one player. Your solution won't
| work when there are multiple players.
| ajankovic wrote:
| I don't think you examined the code in full. main spawns 10
| go routines that are constantly sending player scores to the
| game. That means 10 different players are sending their
| scores concurrently until someone reaches score of 100.
| boruto wrote:
| Quite a change in mood of comments compared to when it was posted
| last time. https://news.ycombinator.com/item?id=11210578
| jtolds wrote:
| Seriously! This caused such a ruckus when I posted this 9 years
| ago. I lost some professional acquaintanceships over it!
| Definitely a different reception.
| dingdingdang wrote:
| If channels are the wrong way to do things in Golang, what is the
| right way?
| fpoling wrote:
| In Go in many cases channels are unavoidable due to API. As
| already was pointed out in other threads a good rule of thumb
| is not to use them in public method signatures.
|
| The valid use case for channels is to signal to consumer via
| channel close in Context.Done() style that something is ready
| that then can be fetched using separated API.
|
| Then if you need to serialize access, just use locks.
|
| WorkGroup can replace channels in surprisingly many cases.
|
| A message passing queue with priorities implemented on top of
| mutexes/signals can be used in many cases that require complex
| interactions between many components.
| dingdingdang wrote:
| Cheers for clear summary, I take it you mean sync.WaitGroup
| and not WorkGroup? As a beginner I started up with WaitGroup
| usage years ago since it was more intuitive to me.. worked
| fine afaict.
| nasretdinov wrote:
| I think since a concept of channel was something new and exciting
| back when Go was introduced, people (including myself) tried
| using it everywhere they could. Over time, as you collect your
| experience with the tool you get better at it, and certainly for
| shared state management channels are rarely the best option,
| however there still are quite a few places where you can't do
| something equivalent to what channels provide easily, which is to
| block until you've received new data. It just so happens that
| those situations are quite rare in Go.
| codr7 wrote:
| Agreed, channels are overrated and overused in Go.
|
| Like closures, channels are very flexible and can be used to
| implement just about anything; that doesn't mean doing so is a
| good idea.
|
| I would likely reach for atomics before mutexes in the game
| example.
___________________________________________________________________
(page generated 2025-04-13 23:01 UTC)