When would you reach for bytes.Buffer instead of strings.Builder in Go?
answer
- one of them can be read back
- which one satisfies io.Reader?
- String is free on one, a copy on the other
- Bytes hands you the buffer's own array
- Reset keeps the capacity on only one
basics
~20 sReach for bytes.Buffer when you need to read the accumulated bytes back, hand them to something expecting an io.Reader, or keep the capacity across a Reset. Use strings.Builder when the result is a string: its String method avoids the final copy.
solid answer
~40 sBoth accumulate bytes and both have usable zero values, but they are shaped for different endings. `strings.Builder` is write-only and optimised for producing a `string`: `String()` hands the accumulated bytes over without copying them. `bytes.Buffer` is a read-write byte accumulator — it implements `io.Reader` as well as `io.Writer`, plus `ReadFrom`, `WriteTo`, `ReadString`, `Next` and `Truncate` — so it works as a pipe between an producer and a consumer. Its `String()` **does** copy the unread bytes into a new string, while `Bytes()` returns a slice aliasing the buffer, valid only until the next modification. `Reset` also differs: `bytes.Buffer.Reset` keeps the backing array, so a reused buffer stops allocating, whereas `strings.Builder.Reset` drops it. Neither is safe for concurrent use, and neither should be copied after its first write — only the builder panics when you do.
code
go · 9 linesvar b strings.Builder
b.WriteString("SELECT 1")
s := b.String() // hands over the accumulated bytes, no copy
var buf bytes.Buffer
buf.WriteString("SELECT 1")
t := buf.String() // copies the unread bytes into a new string
p := buf.Bytes() // aliases the buffer: valid only until the next modification
n, err := buf.Read(make([]byte, 4)) // *bytes.Buffer is also an io.Readergo deeper
Know the headline: strings.Builder is for producing a string, bytes.Buffer for bytes you may also need to read back. Both work straight from their zero value with no constructor.
Explain the interface difference — *bytes.Buffer is an io.Reader as well as an io.Writer — and the two endings: Builder.String avoids a copy, Buffer.String makes one, and Buffer.Bytes aliases the buffer.
Demonstrate the aliasing and reuse judgment: when a returned Bytes() slice is a latent bug, and when retained capacity across Reset makes bytes.Buffer the right accumulator in a hot path.
Own the API-shape call: whether your package hands callers a string, a []byte it may reuse, or an io.Reader, and what that commits you to once other teams depend on the signature.
## Two accumulators, two endings `strings.Builder` (in `strings`) and `bytes.Buffer` (in `bytes`) both let you build up bytes incrementally without re-copying everything on each append, and both have a usable zero value: `var b strings.Builder` and `var buf bytes.Buffer` are ready immediately. The choice between them is decided by what happens to the bytes at the end. ## strings.Builder: write-only, ends in a string The builder exposes only appends — `Write`, `WriteString`, `WriteByte`, `WriteRune` — plus `Grow`, `Len`, `Cap`, `Reset` and `String`. There is no way to read bytes back out of it, and it satisfies `io.Writer` but not `io.Reader`. Its advantage is the ending. `String()` returns the accumulated bytes as a string **without copying them**, which is safe because the builder only ever appends past bytes it has already emitted. If your function's job is to produce a string, that saved copy is real, and it grows with the size of the result. Its costs are the copy check (a builder that is copied after its first write panics on the next write) and `Reset`, which discards the backing slice entirely rather than truncating it. ## bytes.Buffer: a read-write byte accumulator `bytes.Buffer` is the older, larger type, and it is really a variable-sized buffer of bytes with a read cursor. Beyond the same write methods it offers: - `Read`, `ReadByte`, `ReadRune`, `ReadString(delim byte)`, `ReadBytes`, `Next(n int)` — consuming reads that advance the cursor. - `ReadFrom(r io.Reader)` and `WriteTo(w io.Writer)` — bulk transfer without an intermediate slice. - `Bytes()`, `Truncate(n int)`, `Len()`, `Cap()`, `Grow(n)`, `Reset()`. Because of those, `*bytes.Buffer` satisfies `io.Reader`, `io.Writer`, `io.ReaderFrom`, `io.WriterTo`, `io.ByteReader`, `io.RuneReader` and more. That is the decisive reason to pick it: any API that wants to *read* from you — an encoder that takes an `io.Reader`, a request body, a hash — can take a `*bytes.Buffer` directly, and `strings.Builder` cannot be handed to it. ## The two endings, precisely `buf.String()` copies the unread bytes into a fresh string; there is no way around that, because the buffer may keep mutating afterwards. `buf.Bytes()` avoids the copy but returns a slice **aliasing the buffer's own array**, and the docs are explicit that it is valid only until the next buffer modification. Hold on to it across a later `Write`, `Read`, `Truncate` or `Reset` and you may be looking at bytes that have been overwritten or at an array the buffer has abandoned. If the bytes must outlive the buffer, copy them. This is the single most common bug in code that uses `bytes.Buffer`: stashing `buf.Bytes()` in a struct or a map while the buffer is reused. ## Reset is not the same operation on both `bytes.Buffer.Reset` sets the length back to zero while keeping the backing array, so a buffer reused across iterations warms up once and then stops allocating. `strings.Builder.Reset` drops its slice, so the next write allocates again. If your reuse strategy depends on retained capacity, `bytes.Buffer` is the type that supports it. ## What they share Neither is safe for concurrent use; concurrent writers need your own synchronisation. Neither should be copied once written to — the difference is that the builder detects it and panics, while a copied buffer silently shares one array between two independent lengths and corrupts the result. Both implement `io.Writer`, so `fmt.Fprintf(&b, ...)` works with either, taking the pointer. ## Choosing, in one line each - Producing a string from pieces, then returning it: `strings.Builder`. - Needing to read the bytes back, stream them, or satisfy an `io.Reader`: `bytes.Buffer`. - Reusing one accumulator across many iterations and wanting the capacity to persist: `bytes.Buffer`. - Wanting the compiler-adjacent guard against accidental copies: `strings.Builder`. When the answer really is "either", prefer `strings.Builder` for string results — it is the narrower type, and narrower types make the intent clearer to the next reader.
- How long is the slice returned by bytes.Buffer.Bytes() valid?Only until the next modification of that buffer — a `Write`, a consuming read, `Truncate` or `Reset`. It aliases the buffer's own backing array, so after any of those it may point at overwritten bytes or at an array the buffer has abandoned. If the bytes must outlive that point, copy them into a slice you own.
- Are strings.Builder and bytes.Buffer safe for use from several goroutines?Neither is. Both are plain structs with no internal synchronisation, so concurrent writes are a data race and the race detector will flag them. If several producers must feed one accumulator, guard it yourself or give each producer its own and combine the results afterwards.
- Can fmt.Fprintf write into either type?Yes — both `*strings.Builder` and `*bytes.Buffer` implement `io.Writer`, so `fmt.Fprintf(&b, "...", args...)` targets either. Note the `&`: the write methods have pointer receivers, so passing the value rather than its address will not compile, and for a builder it would be a copy anyway.
saying these in an interview costs you the question
- Says bytes.Buffer.String is copy-free like the builder's
- Stores the slice from Bytes() and reuses the buffer
- Thinks strings.Builder can be read from with Read
- Claims bytes.Buffer needs a constructor before use
- Assumes strings.Builder.Reset retains the capacity