Skip to content

Commit e671fe0

Browse files
dsnetgopherbot
authored andcommitted
bytes: add Buffer.Available and Buffer.AvailableBuffer
This adds a new Buffer.AvailableBuffer method that returns an empty buffer with a possibly non-empty capacity for use with append-like APIs. The typical usage pattern is something like: b := bb.AvailableBuffer() b = appendValue(b, v) bb.Write(b) It allows logic combining append-like APIs with Buffer to avoid needing to allocate and manage buffers themselves and allows the append-like APIs to directly write into the Buffer. The Buffer.Write method uses the builtin copy function, which avoids copying bytes if the source and destination are identical. Thus, Buffer.Write is a constant-time call for this pattern. Performance: BenchmarkBufferAppendNoCopy 2.909 ns/op 5766942167.24 MB/s This benchmark should only be testing the cost of bookkeeping and never the copying of the input slice. Thus, the MB/s should be orders of magnitude faster than RAM. Fixes #53685 Change-Id: I0b41e54361339df309db8d03527689b123f99085 Reviewed-on: https://go-review.googlesource.com/c/go/+/474635 Run-TryBot: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Daniel Martí <mvdan@mvdan.cc> Reviewed-by: Cherry Mui <cherryyz@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Ian Lance Taylor <iant@google.com>
1 parent bcd8161 commit e671fe0

File tree

4 files changed

+65
-0
lines changed

4 files changed

+65
-0
lines changed

api/next/53685.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pkg bytes, method (*Buffer) Available() int #53685
2+
pkg bytes, method (*Buffer) AvailableBuffer() []uint8 #53685

src/bytes/buffer.go

+9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ const maxInt = int(^uint(0) >> 1)
5353
// so immediate changes to the slice will affect the result of future reads.
5454
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }
5555

56+
// AvailableBuffer returns an empty buffer with b.Available() capacity.
57+
// This buffer is intended to be appended to and
58+
// passed to an immediately succeeding Write call.
59+
// The buffer is only valid until the next write operation on b.
60+
func (b *Buffer) AvailableBuffer() []byte { return b.buf[len(b.buf):] }
61+
5662
// String returns the contents of the unread portion of the buffer
5763
// as a string. If the Buffer is a nil pointer, it returns "<nil>".
5864
//
@@ -76,6 +82,9 @@ func (b *Buffer) Len() int { return len(b.buf) - b.off }
7682
// total space allocated for the buffer's data.
7783
func (b *Buffer) Cap() int { return cap(b.buf) }
7884

85+
// Available returns how many bytes are unused in the buffer.
86+
func (b *Buffer) Available() int { return cap(b.buf) - len(b.buf) }
87+
7988
// Truncate discards all but the first n unread bytes from the buffer
8089
// but continues to use the same allocated storage.
8190
// It panics if n is negative or greater than the length of the buffer.

src/bytes/buffer_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"math/rand"
12+
"strconv"
1213
"testing"
1314
"unicode/utf8"
1415
)
@@ -326,6 +327,33 @@ func TestWriteTo(t *testing.T) {
326327
}
327328
}
328329

330+
func TestWriteAppend(t *testing.T) {
331+
var got Buffer
332+
var want []byte
333+
for i := 0; i < 1000; i++ {
334+
b := got.AvailableBuffer()
335+
b = strconv.AppendInt(b, int64(i), 10)
336+
want = strconv.AppendInt(want, int64(i), 10)
337+
got.Write(b)
338+
}
339+
if !Equal(got.Bytes(), want) {
340+
t.Fatalf("Bytes() = %q, want %q", got, want)
341+
}
342+
343+
// With a sufficiently sized buffer, there should be no allocations.
344+
n := testing.AllocsPerRun(100, func() {
345+
got.Reset()
346+
for i := 0; i < 1000; i++ {
347+
b := got.AvailableBuffer()
348+
b = strconv.AppendInt(b, int64(i), 10)
349+
got.Write(b)
350+
}
351+
})
352+
if n > 0 {
353+
t.Errorf("allocations occurred while appending")
354+
}
355+
}
356+
329357
func TestRuneIO(t *testing.T) {
330358
const NRune = 1000
331359
// Built a test slice while we write the data
@@ -687,3 +715,16 @@ func BenchmarkBufferWriteBlock(b *testing.B) {
687715
})
688716
}
689717
}
718+
719+
func BenchmarkBufferAppendNoCopy(b *testing.B) {
720+
var bb Buffer
721+
bb.Grow(16 << 20)
722+
b.SetBytes(int64(bb.Available()))
723+
b.ReportAllocs()
724+
for i := 0; i < b.N; i++ {
725+
bb.Reset()
726+
b := bb.AvailableBuffer()
727+
b = b[:cap(b)] // use max capacity to simulate a large append operation
728+
bb.Write(b) // should be nearly infinitely fast
729+
}
730+
}

src/bytes/example_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"os"
1313
"sort"
14+
"strconv"
1415
"unicode"
1516
)
1617

@@ -37,6 +38,18 @@ func ExampleBuffer_Bytes() {
3738
// Output: hello world
3839
}
3940

41+
func ExampleBuffer_AvailableBuffer() {
42+
var buf bytes.Buffer
43+
for i := 0; i < 4; i++ {
44+
b := buf.AvailableBuffer()
45+
b = strconv.AppendInt(b, int64(i), 10)
46+
b = append(b, ' ')
47+
buf.Write(b)
48+
}
49+
os.Stdout.Write(buf.Bytes())
50+
// Output: 0 1 2 3
51+
}
52+
4053
func ExampleBuffer_Cap() {
4154
buf1 := bytes.NewBuffer(make([]byte, 10))
4255
buf2 := bytes.NewBuffer(make([]byte, 0, 10))

0 commit comments

Comments
 (0)