What does `semaphore.Weighted` from golang.org/x/sync give you that a buffered `chan struct{}` cannot?
answer
- not every unit costs the same
- waiting you are allowed to give up
- one int64 in, the same int64 back
- acquire takes a context, returns error
basics
~20 sTwo things: weights, so one unit can consume several units of capacity rather than exactly one, and a context-aware Acquire that returns an error when the caller's context is cancelled. A channel semaphore counts identical, uncancellable permits.
solid answer
~50 sA channel semaphore counts units: every acquire costs exactly one slot. `semaphore.NewWeighted(n)` gives a total of n units, and `Acquire(ctx, w)` takes w of them at once, so a hundred-megabyte upload part can cost more than a one-megabyte part and the limit becomes a bound on total in-flight size rather than on item count. `Acquire` also takes a `context.Context` and returns an error, so a waiter can be cancelled or time out without you hand-rolling a `select`; `TryAcquire(w)` is the non-blocking form returning a bool, and `Release(w)` gives the weight back. The cost is a dependency and a sharper edge: `Release` panics if you return more than is held, and an `Acquire` for more than the total blocks until the context is done rather than failing fast. When units are interchangeable, the channel stays the better answer.
go deeper
Recall the two differences: weight per acquire instead of one slot each, and an acquire that takes a context so a waiter can be cancelled. Know that the plain channel version is still the common case.
Explain the API shape — a total, a weight per acquire, a matching release — and why an unbalanced release is a panic rather than a silent miscount. Be able to say why a channel cannot emulate weights.
Show judgment about when varying unit cost makes a count-based limit meaningless, and name the sharp edges you would guard against: an oversized request that waits out its context, and a large head-of-queue waiter delaying smaller ones.
Weigh a dependency against four lines of built-in language. Argue when a team should standardise on one cancellable helper rather than letting each call site invent its own acquire.
## What a channel semaphore cannot express `make(chan struct{}, N)` counts occupancy in slots of one. That is a complete counting semaphore, and for interchangeable units it is the right tool. It fails in two situations. **Units of different cost.** Consider a bulk uploader pushing parts to object storage, where parts range from a few kilobytes to hundreds of megabytes. "At most eight parts in flight" bounds nothing useful: eight small parts is trivial, eight huge ones is the memory and bandwidth spike you were trying to avoid. What you actually want to bound is total bytes in flight. **Waiting that must be abandonable.** A bare `sem <- struct{}{}` has no deadline. You can wrap it in a `select` with `ctx.Done()`, and for one call site that is fine, but every call site must remember, and there is no reporting of *why* the acquire failed beyond what you write yourself. ## The weighted semaphore `golang.org/x/sync/semaphore` — part of the `golang.org/x` quasi-standard repositories — provides `Weighted`: - `semaphore.NewWeighted(n int64) *semaphore.Weighted` — a semaphore with a total of `n` units. - `(*Weighted).Acquire(ctx context.Context, n int64) error` — takes `n` units, blocking until they are free or `ctx` is done. On cancellation it returns the context's error and acquires **nothing**, so there is no partial state to unwind. - `(*Weighted).TryAcquire(n int64) bool` — takes `n` units if they are free right now, otherwise returns false without blocking. - `(*Weighted).Release(n int64)` — returns `n` units. With it, the uploader's limit becomes a byte budget: create the semaphore with the total bytes you will allow in flight, and acquire each part's size. ## The behaviours that surprise people **Release is unforgiving.** `Release` panics if the released weight exceeds what is currently held. That is a deliberate design choice — an unbalanced release corrupts the count silently in most implementations, so this one fails loudly — but it means the same discipline as the channel version applies: acquire, then immediately `defer sem.Release(w)` with exactly the weight you acquired. **An oversized acquire waits rather than failing.** If you ask for more weight than the semaphore's total, the request can never be satisfied. Rather than deadlocking other callers behind an impossible waiter, it waits for the context to be done and then returns the context's error. So an acquire that "hangs" may in fact be asking for more than exists — clamp or reject sizes above the total yourself if that is possible in your input. **Waiters are served in order, and a large one holds the queue.** The waiter list is FIFO, and when weight is released the semaphore wakes waiters from the front and stops at the first one that does not fit. It deliberately does not skip ahead to a smaller request behind it, because doing so would starve large requests under sustained load. The consequence: a big waiter at the head makes smaller ones that *would* fit wait too. That is a fairness choice, and it is the right one, but it means throughput is not maximised at every instant. **A fast path only when nobody is waiting.** An acquire succeeds immediately only if the weight is free *and* there are no queued waiters, which prevents new arrivals from barging past the queue. ## Choosing between the two Reach for the channel when the units are interchangeable, the acquire site is one place you control, and adding a dependency for four lines of behaviour is not worth it. Reach for the weighted semaphore when cost per unit genuinely varies by an order of magnitude, or when many call sites need a cancellable acquire and you would rather have one well-tested implementation than a `select` copied everywhere. A useful middle position: keep the channel, and make the acquire cancellable once, inside a small helper that returns a release function. That gives most of the ergonomics without the dependency — but it still cannot express weights, and hand-rolling weighted accounting on top of a channel is where people write their own broken semaphore. ## The thing you cannot fake with a channel Sending `w` values into a channel to "acquire w permits" is not an atomic acquire. Two callers each part-way through their sends can hold enough slots between them that neither can finish, and both block forever holding what they took. If you need weights, use something that takes them atomically.
- How would you get a cancellable acquire out of a plain channel semaphore?Wrap the send in a `select`: one case is `sem <- struct{}{}`, the other `<-ctx.Done()`, returning `ctx.Err()`. Register the deferred release only inside the successful case — releasing a permit you never acquired either miscounts the limit or blocks the releaser on an empty channel.
- What happens if you release more weight than you hold on a `semaphore.Weighted`?It panics rather than silently corrupting the count, so an unbalanced release fails loudly at the point of the bug. The discipline is the same as with a channel: acquire, then immediately defer the release with exactly the weight you acquired, never a rounded or recomputed number.
- Why not send several values into a channel semaphore to emulate a weight?Because the sends are not atomic. Two callers each part-way through their sends can hold enough slots between them that neither can complete, and both block forever while holding what they took. A weighted acquire has to take all its units under one lock or none.
- Can a large weighted request be starved by a stream of small ones?No. The waiter list is FIFO and the semaphore stops waking waiters at the first one that does not fit, rather than skipping ahead to smaller requests. That protects large requests from starvation, at the cost of small ones waiting behind a large head-of-queue waiter.
saying these in an interview costs you the question
- Believes sending several values into a channel is an atomic weighted acquire
- Thinks a bare channel send can be cancelled by a context
- Releases a rounded or recomputed weight instead of the acquired one
- Assumes an oversized acquire returns an error immediately
- Reaches for weights when every unit costs the same