# ringbuffer: a Go ring buffer that can stand in for io.Pipe

> smallnest's ringbuffer is a thread-safe circular buffer that implements io.ReaderWriter, and the interesting part is that it can be flipped between non-blocking and blocking behaviour, or handed out as a Pipe, so a producer and consumer can run concurrently without a goroutine handoff per write.

**smallnest/ringbuffer** — a thread-safe circular buffer (ring buffer) in Go, implemented io.ReaderWriter interface

- Repository: https://github.com/smallnest/ringbuffer
- Stars: 650 · Forks: 71
- Language: Go
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/smallnest-ringbuffer

## A circular buffer that implements io.ReaderWriter

ringbuffer is a circular buffer in Go, implemented with the io.ReaderWriter interface, and the README's usage example is short enough to take in at once.

```go
package main

import (
	"fmt"

	"github.com/smallnest/ringbuffer"
)

func main() {
	rb := ringbuffer.New(1024)

	// write
	rb.Write([]byte("abcd"))
	fmt.Println(rb.Length())
	fmt.Println(rb.Free())

	// read
	buf := make([]byte, 4)
	rb.Read(buf)
	fmt.Println(string(buf))
}
```

The constructor takes a capacity, and the buffer reports its current length and its free space, so you can see how full it is. There is also a way to use an existing buffer by replacing New with NewBuffer, which is what you want when the memory is already allocated by something else, for instance a slice from a pool. The project is MIT licensed, the module requires Go 1.26, and the repository is small, with the implementation in ring_buffer.go, the pipe behaviour in pipe.go, and a set of test files whose names say what they cover, including issue23_test.go, notify_test.go, peekat_test.go, readfrom_idle_test.go and a benchmark file.

## Non-blocking by default, and how to turn blocking on

The default behaviour is the opposite of what a channel does, and the README is explicit about it. The default behaviour of the ring buffer is non-blocking, meaning that reads and writes return immediately with an error if the operation cannot be completed. If you want to block when reading or writing, you must enable it.

```go
	rb := ringbuffer.New(1024).SetBlocking(true)
```

With blocking enabled the buffer behaves like a buffered io.Pipe. Regular reads block until data is available, but they do not wait for a full buffer. Writes block until there is space, and a write larger than the buffer waits for reads to make space rather than failing. The non-blocking operations TryRead and TryWrite remain available even in blocking mode, so a consumer can opportunistically take what is there without committing to wait. This two-mode design is the core of the library, and it is the reason a caller has to think about which mode it wants rather than getting one behaviour by default.

## Closing, errors that are stateful, and deadlines

The end of a stream is signalled by closing the writer side with rb.CloseWriter(). Either side can use rb.CloseWithError(err error) to signal an error and close the buffer, and any reads or writes return that error on the next call. The subtle part is in blocking mode, where errors are stateful: the same error is returned until rb.Reset() is called. That is a deliberate design, because a stateful error means a consumer that retries gets the same answer rather than a fresh nil, but it also means the buffer is effectively unusable until someone resets it. Deadlines are supported for blocking reads and writes through rb.WithDeadline(time.Duration), which is the escape hatch for a consumer that cannot wait forever. The reset behaviour is worth pairing with the error behaviour in your own code, because a buffer that has been closed with an error will keep returning it.

## Notify is a wakeup, not a broadcast

The Notify section describes a case that blocking reads cannot cover. A consumer that must wait on several things at once, such as new data, a shutdown signal and a deadline, cannot block in Read, and neither can a consumer that wants to look at buffered data without taking it, since Peek is non-blocking by design. Notify gives those consumers a channel to wait on.

```go
for {
	select {
	case <-rb.Notify():
	case <-ctx.Done():
		return ctx.Err()
	}

	// A wakeup means "look", not "there is data".
	n, err := rb.Peek(scratch)
	// inspect scratch[:n] and consume only what is confirmed
}
```

The channel is signalled when data is written, and when the buffer is closed or reset. It has capacity 1 and signals coalesce, so a wakeup reports only that something changed, never how much or how often, and spurious signals are permitted. Notify is not a broadcast: one signal wakes one waiter, so you should use a single waiting consumer per buffer. The comment in the example is the takeaway, a wakeup means look, not there is data, which is why the code peeks and then consumes only what it has confirmed.

## Replacing io.Copy, and the interface that makes it possible

The ring buffer can replace io.Copy and io.CopyBuffer to do asynchronous copying through the buffer, and the point of that is that the copy operation happens directly on the buffer, so between reads and writes there is no memory copy.

```go
func saveWebsite(url, file string) {
    in, _ := http.Get(url)
    out, _ := os.Create(file)

    // Copy with regular buffered copy
    // n, err := io.Copy(out, in.Body)

    // Copy with ring buffer
    n, err := ringbuffer.New(1024).Copy(out, in.Body)
    fmt.Println(n, err)
}
```

The mechanism is the io.ReaderFrom and io.WriterTo interfaces, which allow the buffer to fill either the write side or the read side respectively, and the README says these functions require that blocking is set on the pipe. The same interfaces are what let a producer read straight into the buffer from a goroutine, with the writer side closed afterwards.

```go
func readWebsite(url string) io.ReadCloser {
	in, _ := http.Get(url)

	// Create blocking ring buffer
	ring := ringbuffer.New(1024).SetBlocking(true)

	// Read from the input in a goroutine into the ring buffer
	go func() {
		ring.ReadFrom(in.Body)
		ring.CloseWriter()
	}()
	return ring.ReadCloser()
}
```

The result is that a slow HTTP body no longer needs its own goroutine per read, and the consumer gets a ReadCloser it can hand to anything expecting one.

## Pipe mode, and the detail that it silently changes blocking

The ring buffer can be used as a compatible but asynchronous replacement for io.Pipe, with the important difference that reads and writes go to the ring buffer rather than being handed off directly. Writes complete as long as the data fits within the buffer, and reads attempt to satisfy reads with data from the buffer, blocking only if the buffer is empty. Where the read and write sides can run concurrently, it is safe to replace io.Pipe with (*Ringbuffer).Pipe().

```go
func main() {
	// Create pipe from a 4KB ring buffer.
	r, w := ringbuffer.New(4 << 10).Pipe()

	go func() {
		fmt.Fprint(w, "some io.Reader stream to be read\n")
		w.Close()
	}()

	if _, err := io.Copy(os.Stdout, r); err != nil {
		log.Fatal(err)
	}
}
```

One thing to note is that when the pipe is created, the ring buffer is internally switched to blocking mode. Error reporting on Close and CloseWithError is similar to io.Pipe. It is also possible to use the original ring buffer alongside the pipe functions, so you can seed the buffer with data and let reads complete at once. The project is young in version terms, at v0.2.0 released on 2026-10-19, with the last push the same day, and the Makefile carries the usual targets including a race detector run, coverage output, a treemap target, lint and vet steps.

## Conclusion

ringbuffer fits a Go service with a producer and a consumer that need to run concurrently and want the producer to block on a full buffer rather than drop, or want a wait-without-blocking primitive for a select loop. It does not fit a workload that needs a broadcast to several waiters, because Notify wakes one waiter per signal and the channel has capacity one. Verify first that you want blocking semantics at all, since the default is non-blocking and returns an error rather than waiting, and remember that the pipe constructor switches the buffer into blocking mode internally, which changes the behaviour of the underlying buffer if you also use it directly.

## FAQ

### How do I create a ring buffer in Go?

Call ringbuffer.New with a capacity, for example ringbuffer.New(1024), and then write and read through it as an io.ReaderWriter. The README also notes you can use an existing buffer by replacing New with NewBuffer, and that Length and Free report how much is buffered.

### Is the ring buffer blocking by default?

No. The default behaviour is non-blocking, so reads and writes return immediately with an error if the operation cannot be completed. You enable blocking with SetBlocking(true), after which it behaves like a buffered io.Pipe. TryRead and TryWrite remain available for non-blocking operations in either mode.

### What is Notify for in the ring buffer?

Notify gives a channel for consumers that must wait on several things at once, since those cannot block in Read, and for consumers that want to Peek without taking data. The channel has capacity 1, signals coalesce, and it is not a broadcast, so one signal wakes one waiter.

### How do I close the ring buffer and report an error?

Close the writer side with rb.CloseWriter() to end the stream, and use rb.CloseWithError(err) from either side to close with an error that reads and writes return on the next call. In blocking mode the error is stateful and is returned repeatedly until rb.Reset() is called.

## Sources

- [Issues](https://github.com/smallnest/ringbuffer/issues)
- [License: MIT](https://github.com/smallnest/ringbuffer/blob/master/LICENSE)
- [README](https://github.com/smallnest/ringbuffer/blob/master/README.md)
- [Releases](https://github.com/smallnest/ringbuffer/releases)
- [smallnest/ringbuffer on GitHub](https://github.com/smallnest/ringbuffer)

---

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