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. To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/8a063215-e980-4e93-a1b0-aac8bbd786b4n%40googlegroups.com.