# deque and the ring buffer behind a fast Go double-ended queue

> gammazero/deque is a MIT licensed Go package that implements a double-ended queue on a resizable ring buffer, giving O(1) pushes and pops at both ends. It deliberately leaves concurrency safety to the caller and panics when you read an empty deque.

**gammazero/deque** — Fast ring-buffer deque (double-ended queue)

- Repository: https://github.com/gammazero/deque
- Stars: 789 · Forks: 70
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/gammazero-deque

## One container that behaves as a queue or a stack

The problem this solves is narrow and familiar: a container where you need to add and remove at both ends without paying for a slice copy or a linked list node on every operation. The README describes deque as a generalization of a queue and a stack, with O(1) operations at either end.

Which end you use decides the semantics. Queue behaviour, FIFO, comes from pairing `PushBack` with `PopFront`. Stack behaviour, LIFO, comes from pairing `PushBack` with `PopBack`. Nothing else in the API changes, so the same value can be used as a queue in one part of a program and as a stack in another, which is handy for breadth first traversal or undo handling where the traversal order is not known in advance.

The whole implementation lives in a small set of files at the repository root: `deque.go`, `doc.go`, `deque_test.go` and `go.mod`. The module path is `github.com/gammazero/deque` and the module declares Go 1.26.

## The circular buffer resizes by powers of two

The mechanism behind the performance claim is a circular buffer rather than a slice or a linked list. Because the buffer wraps around, space freed by a pop is reused by a later push, so the implementation allocates only when the full capacity has been consumed. The README states that this reduces memory and time overhead and produces fewer GC pauses than implementations based on slices or linked lists.

Resizing follows a fixed rule. The buffer grows when more capacity is needed and shrinks when only a quarter of the capacity is in use, and both growth and shrinkage happen in powers of two. Power of two sizing is what makes the index arithmetic cheap: the README notes that the implementation uses bitwise arithmetic for all calculations, so wrapping an index around the end of the buffer is a mask rather than a modulo. Because each growth doubles the capacity, adding elements only triggers O(log n) allocations over the lifetime of the buffer.

There is a second, more interesting performance condition. The README says the ring buffer performs best when resizes are infrequent, which happens when items moving in and out are balanced, or when the base capacity is large enough that a resize rarely occurs.

## SetBaseCap and Grow remove the resize from the hot path

If you know roughly how many items the container will hold, you can remove resizing from the picture entirely. `SetBaseCap` sets a base capacity, and the README states that this means there is no resizing at or below that specified amount. The effect is one allocation of the right size up front and no growth during normal operation.

`Grow` is the second lever. It ensures sufficient storage for n additional items, so adding that many items afterwards will not trigger a resize. The README describes this as a way to prevent resizing when adding a number of items, which matters in the case where a batch of pushes is known in advance.

Both knobs exist because of how the buffer behaves. A shrink happens when only a quarter of the capacity is used, so a container that is drained and refilled repeatedly can oscillate between sizes. Sizing the base capacity to the working set removes that churn.

Which of the two knobs to use depends on whether you know the working set size in advance. SetBaseCap suits a steady state that stays at or below a known number of items, while Grow suits bursts where a specific count of pushes is coming.

## Reading an empty deque panics by design

This is the sharpest edge in the package, and the README explains the reasoning rather than hiding it. The deque is allowed to hold a `nil` value, so a returned element cannot be used to signal emptiness. The package therefore has two options: panic, or return a second boolean value. It chooses to panic.

The stated rationale is that a boolean return is often ignored by mistake, while a panic is a run-time check that catches the programming error at the point it happens. The README gives the guard explicitly: check `Deque.Len()` before reading from the deque.

The zero value of a deque is a valid, empty, ready-to-use container, as the README example shows with a plain variable declaration. But `Front()` and `Back()` on an empty deque will panic rather than return a zero element, and the pop operations have the same behaviour. Any code path that can empty the deque, including an error path or a partial drain, needs an explicit length check.

## Concurrency safety is deliberately left out

The README is unusually direct about this: for maximum speed, the implementation leaves concurrency safety up to the application, in whatever way the application chooses, if it needs it at all.

That choice is defensible for a low level container, but it means the package ships no mutex, no channel based handoff and no atomic counters. A deque shared between goroutines without external synchronization is a data race, and the race detector will flag it. If you need concurrent access, you have to add the lock yourself, which then means paying the synchronization cost on every operation that the package was designed to keep cheap.

There is a design note worth pairing with that: the iterators. The README says the iterators allow traversal or removal of items, and that removing items this way avoids the intermediate resizes of the internal buffer. That makes a single goroutine able to drain the deque with fewer allocations than repeated `PopFront` calls, which is useful precisely in the case where the application, rather than the container, owns the locking.

## Generics and iterators tie the deque to the standard library

The deque is typed through Go generics, so the element type is fixed at declaration. The README shows both ways of getting a typed instance: a pointer to a new deque, or a plain variable declaration.

```go
    stringDeque := new(deque.Deque[string])
    var intDeque deque.Deque[int]
```

The iterators are the second point of integration. Because the deque can produce an `iter.Seq`, it works with the standard library helpers that consume one, including `slices.Collect` and `slices.Sorted`. That matters for code that wants to snapshot the contents of a deque into a slice without hand written copying loops.

The example program in the README shows the basic lifecycle. Items are pushed at the back, the length is printed, the front and back elements are read, items are popped from both ends, and the deque is then drained from the front until its length reaches zero.

```go
package main

import (
    "fmt"
    "github.com/gammazero/deque"
)

func main() {
    var q deque.Deque[string]
    q.PushBack("foo")
    q.PushBack("bar")
    q.PushBack("baz")

    fmt.Println(q.Len())   // Prints: 3
    fmt.Println(q.Front()) // Prints: foo
    fmt.Println(q.Back())  // Prints: baz

    q.PopFront() // remove "foo"
    q.PopBack()  // remove "baz"

    q.PushFront("hello")
    q.PushBack("world")

    // Consume deque and print elements.
    for q.Len() != 0 {
        fmt.Println(q.PopFront())
    }
}
```

Notice the drain loop checks the length on each pass, which is exactly the guard the README asks for.

## Installing the module and sizing it for a steady workload

The package is distributed as a normal Go module, and the README gives a single command to add it to a project.

```bash
$ go get github.com/gammazero/deque
```

There is no code generation step, no build tag and no C dependency. The whole thing is one Go package, so the import in your file is the only wiring required.

The licence is MIT, with the terms in the LICENSE file at the repository root, which is the permissive end of the spectrum for a library you may embed in a larger service. The last push was on 2026-10-12, and that same day carried the v1.2.2 release, following v1.2.1 in February 2026 and v1.2.0 in November 2025. A version 1 line with regular tagged releases means you can pin an exact version and move deliberately.

One caveat on the module requirement: `go.mod` declares `go 1.26`, so a toolchain older than that cannot build the module as it stands.

## Where a ring buffer deque is the wrong choice

Two cases stand out. The first is concurrent access. Since safety is left to the application, a deque shared across goroutines without a lock is a data race, and if you add a lock you have changed the performance profile the package was built for. If your workload is a producer and consumer pipeline, a channel based queue may serve better, since the channel is the synchronisation point and the deque becomes an internal detail.

The second case is unbounded growth patterns. The buffer shrinks when only a quarter of its capacity is used, which means a deque that repeatedly fills and empties can trigger a cycle of reallocations. The README is candid that the implementation performs best when resizes are infrequent, so workloads that oscillate around a size boundary are the case where `SetBaseCap` or `Grow` is worth setting deliberately rather than leaving to chance.

A third consideration is that the deque is a container, not a queue abstraction. It has no notion of blocking, of bounded capacity with a rejection policy, or of task visibility across consumers. If you need a work queue with those semantics, the deque is the storage layer, not the queue, and you will write the queue part yourself. The README also says nothing about persistence or serialization, so a deque cannot be written to disk or sent over the wire without conversion.

## Conclusion

deque fits Go programs that need cheap pushes and pops at both ends, such as a work queue with a stack like drain, and that want a typed container without allocating interfaces. It does not fit code that needs concurrent access, since the package offers no locking, and it will crash on an unguarded read of an empty deque. Before adopting it, decide whether to set a base capacity with SetBaseCap for your steady state size, and confirm your Go toolchain can build the module, since go.mod declares go 1.26.

## FAQ

### How do I install the gammazero/deque package?

The README gives one command, go get github.com/gammazero/deque, which adds the module to your project. The import path is the same string.

### Is deque safe for concurrent use?

No. The README states that concurrency safety is left up to the application, however it chooses, for maximum speed. You have to add your own synchronisation.

### What happens when I read from an empty deque?

It panics. Because a nil value is allowed inside the deque, the package chose a panic over a second boolean return value. The README advises checking Deque.Len() before reading.

### How can I avoid resizing the internal buffer?

Set a base capacity with SetBaseCap, which prevents resizing at or below that amount, or call Grow to reserve room for n additional items before pushing them.

### Which Go version does the deque module require?

The go.mod file in the repository declares go 1.26, so the module needs a Go toolchain of that version or newer.

## Sources

- [gammazero/deque on GitHub](https://github.com/gammazero/deque)
- [Issues](https://github.com/gammazero/deque/issues)
- [License: MIT](https://github.com/gammazero/deque/blob/main/LICENSE)
- [README](https://github.com/gammazero/deque/blob/main/README.md)
- [Releases](https://github.com/gammazero/deque/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gammazero-deque
