What io.Copy actually allocates
I spent an afternoon convinced that a proxy I had written was allocating per connection, because the heap profile showed a steady climb that tracked concurrency almost exactly. It was, and the allocation was not mine.
io.Copy allocates a 32KB buffer per call unless the source
implements WriterTo or the destination implements
ReaderFrom. For a network socket on either side, neither is true in
the general case, so every concurrent copy holds 32KB that lives until the copy
returns.
buf := make([]byte, 32*1024)
io.CopyBuffer(dst, src, buf)
The fix is io.CopyBuffer with a buffer from a pool. The part
that cost me the afternoon is that CopyBuffer ignores the buffer
entirely if either side implements those interfaces, so a microbenchmark against
bytes.Buffer shows no difference at all and you conclude the theory
is wrong.
Benchmark against the thing you actually copy.