Ringslice is a type-safe generic ring buffer backed by a Go slice, designed for production use cases like sliding window and streaming workloads.
Most ring buffer implementations focus on storage. Ringslice adds lifecycle hooks
so you can react to what happens as data moves through the buffer.
It also features iter.Seq iteration for idiomatic Go range support.
For pointer-based circular lists and round-robin traversal, see Go’s standard container/ring.
const maxRingCapacity = 128
ring := ringslice.New[string](maxRingCapacity)
ring.Add("generic")
ring.Add("ring")
ring.Add("buffer")
// prints: "generic", "ring", "buffer"
for v := range ring.All() {
fmt.Println(v)
}
// prints: "buffer", "ring", "generic"
for v := range ring.AllDesc() {
fmt.Println(v)
}Ringslice provides callback hooks that let you react to internal lifecycle events.
Called before the value is added to the ring. The value will be rejected if false is returned.
This is particularly useful if you want to exclude values with certain characteristics.
// reject empty strings
ring.OnBeforeAdd(func(value string) bool {
return len(value) > 0
})Called each time the write index wraps around the ring. Useful for logging, flushing, or instrumentation.
ring.OnRotate(func(values []string) {
fmt.Println("lap complete 🏁")
})Called by Flush() before the buffer is cleared. Useful for draining the buffer, persisting elements, or instrumentation.
ring.OnFlush(func(values []string) {
for _, v := range values {
db.Insert(v)
}
})Note: Clear() discards all elements without invoking this callback.
Ringslice implements the json.Marshaler interface, allowing you to serialize the buffer directly with json.Marshal().
ring := ringslice.New[int](8)
ring.Add(1)
ring.Add(2)
ring.Add(3)
buf, err := json.Marshal(ring)This functionality is useful when you want to persist the ring buffer, even though ringslice is inherently volatile. The serialized output preserves insertion order regardless of internal buffer rotation.
When using custom structs as ring values, only exported fields are serialized.
Unexported fields are lost during serialization and zero-initialized during
deserialization. If your struct has unexported fields that must be persisted,
implement MarshalJSON() and UnmarshalJSON() on that struct type to handle
them explicitly.
Ringslice uses a read-write lock to allow multiple concurrent readers while serializing writers. This ensures thread-safety while maintaining high throughput for read-heavy workloads.