skip to content

Why must a bytes.Buffer be reset when it is recycled through a sync.Pool, and what breaks if it isn't?

level: middleimportance: must knowfreq 58%

answer

  1. reused, not cleaned
  2. the last writer's bytes are still there
  3. one line's text lands in another's
  4. Put gives up ownership immediately
  5. Bytes aliases the pooled array

basics

~20 s

sync.Pool hands out objects exactly as the previous user left them. A bytes.Buffer that was never reset still holds the earlier bytes, so the next writer appends to someone else's data — one caller's text leaks into another caller's output.

solid answer

~50 s

A `sync.Pool` stores objects, not clean objects. `Get` returns whatever state the last user left behind, so a `*bytes.Buffer` still carrying the previous log line will have the new line appended to it. In a log encoder that means request A's bytes appear inside request B's output — a correctness bug and, when the buffer held anything sensitive, a disclosure bug. The fix is to call `buf.Reset()` in exactly one agreed place: either immediately after every `Get`, or immediately before every `Put`. Resetting after `Get` is the safer convention, because it also covers objects that some other code path put back dirty. The second half of the rule is ownership: once you call `Put`, the object is no longer yours. Any `[]byte` you took from it with `Bytes()` aliases memory another goroutine may already be overwriting, so copy anything you need out before releasing it.

code

go · 7 lines
go
func (e *Encoder) Line(msg string) []byte {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.WriteString(msg) // appended to whatever the last user left
	out := buf.Bytes()   // aliases the pooled backing array
	bufPool.Put(buf)     // released while out still points into it
	return out
}

go deeper

for a junior

Remember the one-line rule: an object from a pool arrives dirty, so clear it before you use it. Be able to point at where in a small function the Reset call belongs.

for a middle

Explain both halves of the discipline — reset the contents, and give up ownership at Put — and why a slice taken with Bytes() must be copied before release. Know that this failure is not a data race.

for a senior

Treat buffer reuse as a data-boundary change and say how you would prove it safe: helpers that make the reset unskippable, plus a test that acquires, marks, releases and asserts the next acquire carries nothing across.

for a principal

Own the call about whether the hazard is exposed at all. A shared library that exports a raw pool pushes an invisible correctness rule onto every consumer; wrapping it, or declining the optimisation on a cold path, is a defensible decision to make explicit.

## The contract is "reused", not "clean" `sync.Pool` exists to avoid allocating a new object per operation. Avoiding the allocation means avoiding the initialisation that comes with it: a freshly allocated `bytes.Buffer` is empty because it is new, while a pooled one is whatever the last user made of it. Nothing in `Get` or `Put` clears anything. `bytes.Buffer.Reset()` sets the length back to zero while keeping the already-allocated backing array — which is exactly the trade you wanted: keep the memory, drop the contents. ## What the bug looks like Take a logging library that formats each line into a pooled buffer on the hot path. The encoder gets a buffer, writes `level=info msg="payment accepted" card=...`, copies the result to the writer, and puts the buffer back — without resetting. The next caller gets that buffer, writes its own short line, and emits both: the previous request's text with the new line glued onto the end. Under load, unrelated requests' content interleaves in a pattern that looks random and is impossible to reproduce from a single unit test. The reviewer's angle matters here. A buffer-reuse optimisation is a *confidentiality* change, not just a performance change: it deliberately arranges for one request's bytes to survive in memory that another request will write into. Whatever passed through that buffer — tokens, personal data, an internal error message — can end up in an output stream with a different audience. "We reuse buffers across requests" deserves the same scrutiny as any other cross-tenant data path. ## Where to put the Reset Two conventions work, and mixing them is what fails: 1. **Reset after Get.** `buf := pool.Get().(*bytes.Buffer); buf.Reset()`. Defensive: it does not matter what any other code path put back. This is the recommended default. 2. **Reset before Put.** `buf.Reset(); pool.Put(buf)`. Slightly cheaper conceptually — the pool only ever holds clean objects — but a single call site that forgets it poisons the pool for everyone else. A hybrid works well in practice: wrap both ends in package-private `acquire()` and `release(buf)` helpers, and never expose the raw pool. Then there is exactly one place to review, and the rule cannot be forgotten at a new call site. For structs with several fields, the safest reset is assignment of the zero value (`*obj = Thing{}`) followed by re-establishing any retained capacity, so adding a field later cannot leave stale data behind. ## The other half: ownership after Put Resetting is only half the discipline. `Put` transfers ownership. From that instant another goroutine may `Get` the same object and start writing into it. Two mistakes follow: - **Using the object after Put.** Now two goroutines write the same memory. This one *is* a data race, and a `-race` build will report it if the test happens to exercise the interleaving. - **Returning memory that aliases the object.** `buf.Bytes()` returns a slice pointing into the buffer's backing array. Return that to a caller and put the buffer back, and the caller's slice mutates under them. Copy first — `append([]byte(nil), buf.Bytes()...)` — or use `buf.String()`, which copies. Note the asymmetry: the dirty-buffer bug is *not* a data race. The hand-off is properly synchronised; the object is simply carrying the wrong contents. The race detector will say nothing about it, which is why teams that trust `-race` to catch "concurrency bugs" walk straight past it. ## How you actually catch it With a test that models the contract rather than the code path. Write a property-style test that repeatedly acquires a buffer, writes a distinctive marker into it, releases it, acquires again, and asserts the buffer it gets back has length zero and contains none of the previous marker. Force the pool to recycle by running the loop many times, and run it under `-race` as well to catch the use-after-Put variant. It is a cheap test, it fails loudly the day someone adds a call site that skips the reset, and it is the thing to show a reviewer who is being asked to approve buffer reuse. ## Summary Reset because the pool guarantees reuse, not cleanliness. Reset in one place, ideally behind acquire/release helpers. Treat `Put` as a release of ownership, and copy anything out that must outlive it.

  • Will the race detector catch a buffer that was put back dirty?
    No. The hand-off through the pool is correctly synchronised, so there is no data race to report — the object is simply carrying stale contents. `-race` only catches the neighbouring bug where you keep writing to a buffer after `Put`. Catch the dirty-buffer case with a test that acquires, writes a marker, releases, acquires again, and asserts nothing carried across.
  • Should Reset be called right after Get or right before Put?
    Either, as long as it is one convention applied everywhere. Resetting after `Get` is more defensive: it protects you from any call site that put a dirty object back. Best is to hide both ends behind package-private `acquire()` and `release()` helpers and never export the pool, so there is a single place to get it right and a single place to review.
  • How would you reset a pooled struct that has several fields rather than a bytes.Buffer?
    Assign the zero value — `*obj = Thing{}` — and then restore anything you deliberately want to keep, such as a slice truncated with `obj.Buf = obj.Buf[:0]` to retain its capacity. Clearing field by field is the version that rots: the next person who adds a field forgets it, and stale data leaks again.

Handing back a whiteboard without wiping it. The next person writes under your notes and presents both.

saying these in an interview costs you the question

  • Assumes Put or Get clears the object
  • Says the race detector would have caught the dirty buffer
  • Returns buf.Bytes() to a caller after putting the buffer back
  • Resets in some call sites but not others
  • Keeps writing to a buffer after handing it to Put