Unbounded, goroutine-safe FIFO queue that delivers its items through a Go channel. Producers call Add, which never blocks; consumers receive from the channel returned by Next. Items wait in a ring buffer that grows as needed until the channel can take them, so a slow consumer never blocks a producer.
import "github.com/domonda/go-types/queue"
| Identifier | Description |
|---|---|
Queue[T] |
Interface with the methods below, returned by New. |
New[T](options...) |
Starts a queue of items of type T and its pump goroutine. Panics for a ChanLen below 0 or an InitialBufferSize below 1. |
Option |
Interface of the options for New. Its method is unexported, so only ChanLen and InitialBufferSize implement it. |
ChanLen |
Option: capacity of the consumer channel, default DefaultChanLen. ChanLen(0) is a valid unbuffered hand-off. |
InitialBufferSize |
Option: initial length of the ring buffer, default DefaultInitialBufferSize. |
DefaultChanLen |
32. |
DefaultInitialBufferSize |
32. |
| Method | Description |
|---|---|
Add(items...) |
Appends the items in order. Never blocks. A no-op without items and after Close. |
Next() |
The receive-only channel that delivers the items in FIFO order. It is closed by Close. |
Len() |
Number of items in the ring buffer plus the items staged in the channel. |
Cap() |
Channel capacity plus the current ring buffer length. |
Close() |
Shuts the queue down, see below. |
A background pump goroutine moves items one at a time from the ring buffer into the channel. It is the only sender and closer of the channel, which keeps delivery strictly FIFO. When the channel is full, the pump blocks in the send and does not spin.
Close discards every item that has not been received yet, closes the channel and waits until the pump goroutine has exited. After Close returns:
- The items still in the ring buffer and in the channel are gone. A consumer that receives concurrently with
Closemay still get some of the items staged in the channel, up toChanLen, before they are discarded. - A
range q.Next()loop ends. Addis a no-op, the items are dropped silently.- Calling
Closeagain returns without effect, so a deferredCloseafter an explicit one is safe. Cap()returns the channel capacity only, because the ring buffer was dropped.
There is no drain operation. To process every item before shutting down, count the items the consumer has handled, for example with a sync.WaitGroup, and call Close after the last one, as in the first example below.
ChanLenis a throughput setting: a larger channel lets more items sit ready for the consumer and wakes the pump less often. It also bounds how many items a racing consumer can still receive duringClose.InitialBufferSizesets the starting length of the ring buffer. The buffer doubles when it is full and never shrinks.
q := queue.New[string](queue.ChanLen(64), queue.InitialBufferSize(16))defer q.Close()right afterAddinmainloses items:Closediscards everything the consumer has not received yet, andmaincan return before the consumer prints anything.- Waiting for
Len() == 0beforeCloseis not enough. The pump holds one item outside the buffer and the channel while it waits to send it, soLen()can be one lower than the number of undelivered items, and0while an item is still on its way. Count handled items instead. Len()andCap()are snapshots that can change as soon as they return.- With several producers calling
Addconcurrently, the items of each producer keep their order, but the items of different producers interleave. - The ring buffer never shrinks, so a burst of items keeps its memory allocated for the lifetime of the queue. Delivered items are released for garbage collection.
- Each queue runs one goroutine until
Closeis called. A queue that is never closed leaks it.
Process every item, then shut down cleanly:
package main
import (
"fmt"
"sync"
"github.com/domonda/go-types/queue"
)
func main() {
q := queue.New[string]()
items := []string{"a", "b", "c", "d", "e"}
var processed sync.WaitGroup
processed.Add(len(items))
consumerDone := make(chan struct{})
go func() {
defer close(consumerDone)
for item := range q.Next() {
fmt.Println("got:", item)
processed.Done()
}
}()
q.Add(items[:2]...)
q.Add(items[2:]...)
processed.Wait() // every item has been handled, nothing is left to discard
q.Close() // closes the channel, so the consumer's range loop ends
<-consumerDone
fmt.Println("closed, len:", q.Len())
// Output:
// got: a
// got: b
// got: c
// got: d
// got: e
// closed, len: 0
}Close discards queued items:
package main
import (
"fmt"
"github.com/domonda/go-types/queue"
)
func main() {
q := queue.New[int](queue.ChanLen(4), queue.InitialBufferSize(8))
q.Add(1, 2, 3) // nobody reads these
q.Close() // discards the queued items
q.Close() // Close is idempotent
q.Add(4) // Add after Close is a no-op
received := 0
for range q.Next() {
received++
}
fmt.Println("received:", received, "len:", q.Len(), "cap:", q.Cap())
// Output:
// received: 0 len: 0 cap: 4
}- docs index: explanations and how-to guides for the other packages of this module.