I don’t think this is correct. There is only a single select on the consumer side - the order of sends by the producers is already random based on go routine wakeup/scheduling.
> On Sep 2, 2025, at 10:46, Jason E. Aten <j.e.a...@gmail.com> wrote: > > Yes, but not in terms of performance. Using a buffered > channel could provide more "fairness" in the sense of "first-come, first > served". > > If you depend on the (pseudo-randomized) select to decide on which > producer's job gets serviced next, you could increase your response > latency by arbitrarily delaying an early job for.a long time, while a > late arriving job can "jump the queue" and get serviced immediately. > > The buffered channel will preserve some of the arrival order. But--this > is only up to its length--after that the late arrivers will still be randomly > serviced--due to the pseudo random select mechanism. So if you > demand true FIFO for all jobs, then you might well be better served > by using a mutex and and a slice to keep your jobs anyway--such > that the limit on your queue is available memory rather than a > fixed channel buffer size. > > Of course if you over-run all of your memory, you are in trouble > again. As usual, back-pressure is a really critical component > of most designs. You usually want it. > > On Tuesday, September 2, 2025 at 4:33:05 PM UTC+1 Egor Ponomarev wrote: >> Hi Robert, Jason, >> >> Thank you both for your detailed and thoughtful responses — they helped me >> see the problem more clearly. Let me share some more details about our >> specific case: >> >> We have exactly one consumer (worker), and we can’t add more because the >> underlying resource can only be accessed by one process at a time (think of >> it as exclusive access to a single connection). >> >> The worker operation is a TCP connection, which is usually fast, but the >> network can occasionally be unreliable and introduce delays. >> >> We may have lots of producers, and each producer waits for a result after >> submitting a request. >> >> Given these constraints, can an unbuffered channel have any advantage over a >> buffered one for our case? >> My understanding is that producers will just end up blocking when the single >> worker can’t keep up — so whether the blocking happens at “enqueue time” >> (unbuffered channel) or later (buffered channel). >> >> What’s your view — is there any benefit in using an unbuffered/buffered >> channel in this situation? >> >> >> Thanks again for the guidance! >> >> >> понедельник, 1 сентября 2025 г. в 14:04:48 UTC-5, Jason E. Aten: >>> Hi Egor, >>> >>> To add to what Robert advises -- there is no one-size-fits-all >>> guidance that covers all situations. You have to understand the >>> principles of operation and reason/measure from there. There are >>> heuristics, but even then exceptions to the rules of thumb abound. >>> >>> As Robert said, in general the buffered channel will give you >>> more opportunity for parallelism, and might move your bottleneck >>> forward or back in the processing pipeline. >>> >>> You could try to study the location of your bottleneck, and tracing >>> ( https://go.dev/blog/execution-traces-2024 ) might help >>> there (but I've not used it myself--I would just start with a >>> basic CPU profile and see if there are hot spots). >>> >>> An old design heuristic in Go was to always start >>> with unbuffered channels. Then add buffering to tune >>> performance. >>> >>> However there are plenty of times when I >>> allocate a channel with a buffer of size 1 so that I know >>> my initial sender can queue an initial value without itself >>> blocking. >>> >>> Sometimes, for flow-control, I never want to >>> buffer a channel--in particular when going network <-> channel, >>> because I want the local back-pressure to propagate >>> through TCP/QUIC to the result in back-pressure on the >>> remote side, and if I buffer then in effect I'm asking for work I cannot >>> yet handle. >>> >>> If I'm using a channel as a test event history, then I typically >>> give it a massive buffer, and even then also wrap it in a function >>> that will panic if the channel reaches cap() capacity; because >>> I never really want my tests to be blocked on creating >>> a test execution event-specific "trace" that I'm going to >>> assert over in the test. So in that case I always want big buffers. >>> >>> As above, exceptions to most heuristics are common. >>> >>> In your particular example, I suspect your colleague is right >>> and you are not gaining anything from channel buffering--of course >>> it is impossible to know for sure without the system in front >>> of you to measure. >>> >>> Lastly, you likely already realize this, but the request+response >>> wait pattern you cited typically needs both request and waiting >>> for the response to be wrapped in selects with a "bail-out" or shutdown >>> channel: >>> >>> jobTicket := makeJobTicketWithDoneChannel() >>> select { >>> case sendRequestToDoJobChan <- jobTicket: >>> case <-bailoutOnShutDownChan: // or context.Done, etc >>> // exit/cleanup here >>> } >>> select { >>> case <-jobTicket.Done: >>> case <-bailoutOnShutDownChan: >>> // exit/cleanup here >>> } >>> in order to enable graceful stopping/shutdown of goroutines. >>> On Monday, September 1, 2025 at 5:13:32 PM UTC+1 robert engels wrote: >>>> There is not enough info to give a full recommendation but I suspect you >>>> are misunderstanding how it works. >>>> >>>> The buffered channels allow the producers to continue while waiting for >>>> the consumer to finish. >>>> >>>> If the producer can’t continue until the consumer runs and provides a >>>> value via a callback or other channel, then yes the buffered channel might >>>> not seem to provide any value - expect that in a highly concurrent >>>> environment go routines are usually not in a pure ‘reading the channel’ >>>> mode - they are finishing up a previous request - so the buffering allows >>>> some level of additional concurrency in the state. >>>> >>>> When requests are extremely short in duration this can matter a lot. >>>> >>>> Usually though, a better solution is to simply have N+1 consumers for N >>>> producers and use a handoff channel (unbuffered) - but if the workload is >>>> CPU bound you will expend extra resources context switching (ie. >>>> thrashing) - because these Go routines will be timesliced. >>>> >>>> Better to cap the consumers and use a buffered channel. >>>> >>>> >>>> >>>> >>>>> On Sep 1, 2025, at 08:37, Egor Ponomarev <egorvpo...@gmail.com <>> wrote: >>>>> >>>> >>>>> We’re using a typical producer-consumer pattern: goroutines send messages >>>>> to a channel, and a worker processes them. A colleague asked me why we >>>>> even bother with a buffered channel (say, size 1000) if we’re waiting for >>>>> the result anyway. >>>>> >>>>> I tried to explain it like this: there are two kinds of waiting. >>>>> >>>>> >>>>> >>>>> “Bad” waiting – when a goroutine is blocked trying to send to a full >>>>> channel: >>>>> requestChan <- req // goroutine just hangs here, blocking the system >>>>> >>>>> “Good” waiting – when the send succeeds quickly, and you wait for the >>>>> result afterwards: >>>>> requestChan <- req // quickly enqueued >>>>> result := <-resultChan // wait for result without holding up others >>>>> >>>>> >>>>> The point: a big buffer lets goroutines hand off tasks fast and free >>>>> themselves for new work. Under burst load, this is crucial — it lets the >>>>> system absorb spikes without slowing everything down. >>>>> >>>>> But here’s the twist: my colleague tested it with 2000 goroutines and got >>>>> roughly the same processing time. His argument: “waiting to enqueue or >>>>> dequeue seems to perform the same no matter how many goroutines are >>>>> waiting.” >>>>> >>>>> So my question is: does Go have any official docs that describe this >>>>> idea? Effective Go shows semaphores, but it doesn’t really spell out this >>>>> difference in blocking types. >>>>> >>>>> >>>>> Am I misunderstanding something, or is this just one of those “implicit >>>>> Go concurrency truths” that everyone sort of knows but isn’t officially >>>>> documented? >>>>> >>>>> >>>> >>>>> -- >>>>> You received this message because you are subscribed to the Google Groups >>>>> "golang-nuts" group. >>>>> To unsubscribe from this group and stop receiving emails from it, send an >>>>> email to golang-nuts...@googlegroups.com <>. >>>>> To view this discussion visit >>>>> https://groups.google.com/d/msgid/golang-nuts/b4194b6b-51ea-42ff-af34-b7aa6093c15fn%40googlegroups.com >>>>> >>>>> <https://groups.google.com/d/msgid/golang-nuts/b4194b6b-51ea-42ff-af34-b7aa6093c15fn%40googlegroups.com?utm_medium=email&utm_source=footer>. >>>> > > > -- > You received this message because you are subscribed to the Google Groups > "golang-nuts" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to golang-nuts+unsubscr...@googlegroups.com > <mailto:golang-nuts+unsubscr...@googlegroups.com>. > To view this discussion visit > https://groups.google.com/d/msgid/golang-nuts/8a063215-e980-4e93-a1b0-aac8bbd786b4n%40googlegroups.com > > <https://groups.google.com/d/msgid/golang-nuts/8a063215-e980-4e93-a1b0-aac8bbd786b4n%40googlegroups.com?utm_medium=email&utm_source=footer>. -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/818EF6F9-E919-477B-9B86-3DF8287EE9EC%40ix.netcom.com.