bufpool

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 3 Imported by: 0

README

bufpool

A small Go package for pooling and reusing byte buffers, reducing allocations and garbage-collector pressure in code that handles many short-lived buffers.

A Buffer implements io.Reader, io.Writer, io.StringWriter, io.ReaderFrom, io.WriterTo, io.Closer and fmt.Stringer, so it drops into most code that already speaks the standard streaming interfaces. Buffers obtained from a Pool are returned to it for reuse, and an adaptive strike heuristic discards backing arrays that have grown large but are repeatedly under-utilized, so a single large write does not pin memory indefinitely. See COMPARISON.md for how this stacks up against sync.Pool idioms and other buffer pools.

Adapted from https://github.com/golang/go/issues/27735#issuecomment-739169121.

Install

go get github.com/JohanLindvall/bufpool
import "github.com/JohanLindvall/bufpool"

Quick start

var pool bufpool.Pool // the zero value is ready to use

// Get a buffer from the pool.
buf := pool.Get()

buf.WriteString("hello ")
buf.Write([]byte("world"))

// Use it as an io.Reader.
data, _ := bufpool.ReadAllBytes(buf) // []byte("hello world"), zero-copy for *Buffer
process(data)                        // use (or copy) the bytes *before* releasing:
                                     // data aliases the buffer's backing array

// Return the buffer to the pool for reuse. This invalidates data;
// do not use buf or data afterwards.
buf.Release()

Because a Buffer is an io.Closer, it also works with defer, and can be handed off as an io.ReadCloser that returns itself to the pool when closed:

func payload(pool *bufpool.Pool) io.ReadCloser {
    buf := pool.Get()
    buf.WriteString("body")
    return buf // the consumer's Close returns the buffer to the pool
}

Usage

Pooling
var pool bufpool.Pool

buf := pool.Get()

Calling Release (or Close) returns the buffer to the pool and resets it to the zero value, so it must not be used afterwards. Re-acquire one with Get. A Buffer must not be copied after first use (go vet reports such copies).

Reading and writing

A Buffer separates writes (which append) from reads (which consume from the front, tracked by an internal read position):

buf := pool.Get()
buf.WriteString("abcdef")

p := make([]byte, 3)
buf.Read(p)            // p = "abc", read position now at 3
buf.Bytes()            // []byte("def") — the unread remainder (aliases the buffer)
buf.String()           // "def" — a copy, safe to keep after release
buf.Len()              // 3 — unread bytes, like bytes.Buffer.Len
buf.Size()             // 6 — total written length, including consumed bytes
buf.Cap()              // capacity of the backing array

buf.Rewind()           // reset the read position to re-read from the start

WriteTo streams the unread portion to any io.Writer, and ReadFrom fills the buffer from any io.Reader, so io.Copy in either direction avoids intermediate copy buffers:

n, err := io.Copy(buf, resp.Body) // uses buf.ReadFrom, no 32 KiB scratch buffer

When the output size is known in advance, Grow pre-allocates capacity so subsequent writes do not reallocate:

buf.Grow(len(payload))
buf.Write(payload)
Ownership and aliasing

The zero-copy calls trade safety for speed; their rules are:

  • Bytes and ReadAllBytes return slices that alias the buffer. Release, Close and Reset invalidate them — the backing array re-enters the pool and the next Get may overwrite it. Copy the bytes (or use String) if they must outlive the buffer.
  • NewBuffer and SetBytes adopt the given slice as the backing array without copying. Ownership transfers to the buffer (and, once released, to the pool): the caller must not use the slice afterwards.
Detached buffers

A Buffer can be used standalone, without a pool, via NewBuffer:

buf := bufpool.NewBuffer([]byte("seed")) // adopts the slice; don't reuse it
buf.WriteString(" more")

A detached buffer's Release/Close are no-ops, so defer buf.Close() is safe regardless of a buffer's origin. Detach turns a pooled buffer into a detached one — useful when its contents must outlive a consumer that closes it.

In-place reuse

Reset rewinds the read position and truncates the buffer for reuse while keeping it attached to its pool — useful in a tight loop where you want to reuse the same Buffer without round-tripping through Get/Release:

buf := pool.Get()
for _, item := range items {
    buf.Reset()
    buf.WriteString(item)
    // ... use buf ...
}
buf.Release()

The strike heuristic

To avoid keeping unnecessarily large backing arrays alive, both Release and Reset apply the same heuristic when deciding whether to keep a buffer's backing array:

  • Buffers with capacity ≤ 64 KiB are always kept (strike counter cleared).
  • Buffers that are at least 50% utilized are always kept (strike counter cleared).
  • An oversized, under-utilized buffer is given up to four consecutive strikes; on the fifth it is discarded and replaced with a fresh, empty backing array.

This means a single large usage is not kept alive forever by a continuous stream of small ones, while transient large usages are still tolerated.

API overview

Symbol Description
Pool Buffer pool; the zero value is ready to use.
(*Pool) Get() *Buffer Get an empty buffer attached to the pool.
NewBuffer(data []byte) *Buffer Create a detached buffer adopting data (no copy).
(*Buffer) Write / WriteString Append bytes / a string.
(*Buffer) Read / WriteTo Consume the unread portion.
(*Buffer) ReadFrom Fill from an io.Reader until EOF.
(*Buffer) Bytes / String Unread bytes (aliasing) / unread string (copy).
(*Buffer) Len / Size / Cap Unread length / total length / capacity.
(*Buffer) Grow(n int) Pre-allocate space for n more bytes.
(*Buffer) Rewind / Reset Rewind read position / truncate for reuse.
(*Buffer) SetBytes(p []byte) Replace contents, adopting p (no copy), and rewind.
(*Buffer) Release() / Close() error Release into the pool.
(*Buffer) Detach() Detach from the pool; Release/Close become no-ops.
ReadAllBytes(r io.Reader) ([]byte, error) Read all bytes, zero-copy for *Buffer.

See the Go doc comments for the full details of each call.

Performance

Pooling costs a single small allocation per Get/Release cycle (the buffer handle itself); the backing arrays and pool bookkeeping are fully reused, and the write path is allocation-free once capacity is established:

BenchmarkGetRelease-16            38.95 ns/op    64 B/op    1 allocs/op
BenchmarkGetReleaseParallel-16    23.21 ns/op    64 B/op    1 allocs/op
BenchmarkWrite-16                 34.81 ns/op     0 B/op    0 allocs/op

Run them with go test -bench=. -benchmem. For comparisons against bytes.Buffer+sync.Pool, valyala/bytebufferpool and oxtoacart/bpool — including the memory-retention behavior the strike heuristic exists for — see COMPARISON.md; the harness lives in _bench/.

License

MIT

Documentation

Overview

Package bufpool provides pooled, reusable byte buffers that reduce allocations and garbage-collector pressure in code handling many short-lived buffers.

A Buffer implements io.Reader, io.Writer, io.StringWriter, io.ReaderFrom, io.WriterTo, io.Closer and fmt.Stringer. Writes append to the buffer; reads consume it from the front, tracked by an internal read position. Buffers obtained from a Pool (whose zero value is ready to use) are returned to it with Release (or Close), after which they must not be used.

Releasing transfers the backing array back to the pool, so slices obtained through Bytes or ReadAllBytes are invalidated by Release, Close and Reset; conversely, slices handed to NewBuffer or SetBytes are adopted as the buffer's backing array (and follow it into the pool when it is released), so the caller must not use them afterwards.

To keep pooled memory bounded, an adaptive strike heuristic decides on each Release or Reset whether a buffer's backing array is worth keeping: arrays of at most 64 KiB, or at least 50% utilized, are always kept; an oversized, under-utilized array survives up to four consecutive strikes before it is discarded. This prevents a single large usage from pinning memory through a continuous stream of small ones.

Adapted from https://github.com/golang/go/issues/27735#issuecomment-739169121.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ReadAllBytes

func ReadAllBytes(r io.Reader) ([]byte, error)

ReadAllBytes reads all remaining bytes from r. If r is a *Buffer, it returns the buffer's unread bytes directly without copying and advances the buffer to EOF; otherwise it falls back to io.ReadAll. The error is nil on success, mirroring io.ReadAll.

Regardless of r's dynamic type, treat the returned slice as aliasing r's internal storage: it is only valid until r is next written to, reset, released or closed. Copy it if it must outlive r.

Types

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

Buffer is a byte buffer that may be attached to a Pool. It implements io.Reader, io.Writer, io.StringWriter, io.ReaderFrom, io.WriterTo, io.Closer and fmt.Stringer. Writes append to the buffer; reads consume it from the front, tracked by an internal read position. The zero value is a usable, detached buffer.

A Buffer must not be copied after first use (go vet reports such copies), and must not be used after Release or Close.

func NewBuffer

func NewBuffer(data []byte) *Buffer

NewBuffer creates a new detached buffer whose initial contents are data. The slice becomes the buffer's backing array; it is not copied, and the caller should not use data after this call. NewBuffer(nil) creates an empty buffer.

func (*Buffer) Bytes

func (b *Buffer) Bytes() []byte

Bytes returns the unread portion of the buffer. The slice aliases the buffer's backing array and is only valid until the next mutating call; Release, Close and Reset invalidate it. Copy the bytes (or use String) if they must outlive the buffer.

func (*Buffer) Cap

func (b *Buffer) Cap() int

Cap returns the capacity of the buffer's backing array: the total space, including the already-written portion, that can be used before another allocation.

func (*Buffer) Close

func (b *Buffer) Close() error

Close returns the buffer to its pool and always returns a nil error. It implements io.Closer and is equivalent to Release, so a pooled *Buffer can be handed off as an io.ReadCloser and is returned to the pool at the release site without a pool reference in scope.

func (*Buffer) Detach

func (b *Buffer) Detach()

Detach detaches the buffer from its pool, making Release and Close no-ops. Use it to let a buffer's contents safely outlive a consumer that closes it.

func (*Buffer) Grow

func (b *Buffer) Grow(n int)

Grow grows the buffer's capacity, if necessary, to guarantee space for another n bytes: after Grow(n), at least n bytes can be written without another allocation. Grow panics if n is negative or if the buffer would grow beyond the maximum slice length.

func (*Buffer) Len

func (b *Buffer) Len() int

Len returns the number of unread bytes in the buffer, matching the semantics of bytes.Buffer.Len. Use Size for the total written length.

func (*Buffer) Read

func (b *Buffer) Read(p []byte) (int, error)

Read consumes up to len(p) unread bytes into p, advancing the read position. It returns io.EOF once the buffer is fully consumed. Read implements io.Reader.

func (*Buffer) ReadFrom

func (b *Buffer) ReadFrom(r io.Reader) (int64, error)

ReadFrom reads from r until EOF, appending to the buffer and growing it as needed. It returns the number of bytes read and any error except io.EOF encountered during the read. ReadFrom implements io.ReaderFrom, so io.Copy into a Buffer needs no intermediate copy buffer.

func (*Buffer) Release

func (b *Buffer) Release()

Release returns the buffer to its pool and resets it to the zero value. Releasing invalidates all slices previously returned by Bytes or ReadAllBytes: the backing array re-enters the pool and the next Get may overwrite it, so copy such slices first if they must outlive the buffer. After Release the buffer is a detached zero buffer — further calls operate on that empty buffer instead of panicking, but are programming errors. If the buffer is detached, Release is a no-op. The cost of a Get/Release round-trip is the single small allocation of the Buffer handle in Get.

func (*Buffer) Reset

func (b *Buffer) Reset()

Reset rewinds the read position and truncates the buffer for in-place reuse, applying the same keep-or-discard heuristic the pool uses on Release: an oversized, repeatedly under-utilized backing array is dropped (replaced with a fresh nil buffer) instead of kept, so a single large use does not pin memory across resets. Unlike Release, the buffer stays usable and attached to its pool.

func (*Buffer) Rewind

func (b *Buffer) Rewind()

Rewind resets the read position to zero so the buffer's full contents can be read again. It does not modify the contents.

func (*Buffer) SetBytes

func (b *Buffer) SetBytes(p []byte)

SetBytes replaces the buffer's contents with p and rewinds the read position. The slice becomes the new backing array; it is not copied, so ownership of p transfers to the buffer (and, once released, to its pool) and the caller must not use p after this call.

func (*Buffer) Size

func (b *Buffer) Size() int

Size returns the total length of the buffer, including any portion already consumed by Read.

func (*Buffer) String

func (b *Buffer) String() string

String returns a copy of the unread portion of the buffer as a string, implementing fmt.Stringer. If b is nil, it returns "<nil>".

func (*Buffer) Write

func (b *Buffer) Write(p []byte) (int, error)

Write appends p to the buffer, growing the backing array as needed. It always returns len(p) and a nil error. Write implements io.Writer.

func (*Buffer) WriteString

func (b *Buffer) WriteString(s string) (int, error)

WriteString appends s to the buffer without copying it into a temporary []byte first. It always returns len(s) and a nil error. WriteString implements io.StringWriter.

func (*Buffer) WriteTo

func (b *Buffer) WriteTo(w io.Writer) (int64, error)

WriteTo writes the unread portion of the buffer to w, advancing the read position by the number of bytes accepted by w. If w accepts fewer bytes than offered without returning an error, WriteTo returns io.ErrShortWrite. WriteTo implements io.WriterTo.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is a pool of reusable byte buffers, backed by a sync.Pool. The zero value is ready to use. A Pool is safe for concurrent use by multiple goroutines and must not be copied after first use.

func (*Pool) Get

func (p *Pool) Get() *Buffer

Get returns an empty Buffer drawn from the pool. The buffer is attached to p, so calling Release or Close on it puts it back into the pool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL