Jelajahi Sumber

types/strbuilder: add a variant of strings.Builder that uses sync.Pool

... and thus does not need to worry about when it escapes into
unprovable fmt interface{} land.

Also, add some convenience methods for efficiently writing integers.
Brad Fitzpatrick 5 tahun lalu
induk
melakukan
3f4a567032
2 mengubah file dengan 126 tambahan dan 0 penghapusan
  1. 74 0
      types/strbuilder/strbuilder.go
  2. 52 0
      types/strbuilder/strbuilder_test.go

+ 74 - 0
types/strbuilder/strbuilder.go

@@ -0,0 +1,74 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package strbuilder defines a string builder type that allocates
+// less than the standard library's strings.Builder by using a
+// sync.Pool, so it doesn't matter if the compiler can't prove that
+// the builder doesn't escape into the fmt package, etc.
+package strbuilder
+
+import (
+	"bytes"
+	"strconv"
+	"sync"
+)
+
+var pool = sync.Pool{
+	New: func() interface{} { return new(Builder) },
+}
+
+type Builder struct {
+	bb      bytes.Buffer
+	scratch [20]byte // long enough for MinInt64, MaxUint64
+	locked  bool     // in pool, not for use
+}
+
+// Get returns a new or reused string Builder.
+func Get() *Builder {
+	b := pool.Get().(*Builder)
+	b.bb.Reset()
+	b.locked = false
+	return b
+}
+
+// String both returns the Builder's string, and returns the builder
+// to the pool.
+func (b *Builder) String() string {
+	if b.locked {
+		panic("String called twiced on Builder")
+	}
+	s := b.bb.String()
+	b.locked = true
+	pool.Put(b)
+	return s
+}
+
+func (b *Builder) WriteByte(v byte) error {
+	return b.bb.WriteByte(v)
+}
+
+func (b *Builder) WriteString(s string) (int, error) {
+	return b.bb.WriteString(s)
+}
+
+func (b *Builder) Write(p []byte) (int, error) {
+	return b.bb.Write(p)
+}
+
+func (b *Builder) WriteInt(v int64) {
+	b.Write(strconv.AppendInt(b.scratch[:0], v, 10))
+}
+
+func (b *Builder) WriteUint(v uint64) {
+	b.Write(strconv.AppendUint(b.scratch[:0], v, 10))
+}
+
+// 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
+// to the buffer without another allocation. If n is negative, Grow
+// will panic. If the buffer can't grow it will panic with
+// ErrTooLarge.
+func (b *Builder) Grow(n int) {
+	b.bb.Grow(n)
+}

+ 52 - 0
types/strbuilder/strbuilder_test.go

@@ -0,0 +1,52 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package strbuilder
+
+import (
+	"math"
+	"testing"
+)
+
+func TestBuilder(t *testing.T) {
+	const want = "Hello, world 123 -456!"
+	bang := []byte("!")
+	var got string
+	allocs := testing.AllocsPerRun(1000, func() {
+		sb := Get()
+		sb.WriteString("Hello, world ")
+		sb.WriteUint(123)
+		sb.WriteByte(' ')
+		sb.WriteInt(-456)
+		sb.Write(bang)
+		got = sb.String()
+	})
+	if got != want {
+		t.Errorf("got %q; want %q", got, want)
+	}
+	if allocs != 1 {
+		t.Errorf("allocs = %v; want 1", allocs)
+	}
+}
+
+// Verifies scratch buf is large enough.
+func TestIntBounds(t *testing.T) {
+	const want = "-9223372036854775808 9223372036854775807 18446744073709551615"
+	var got string
+	allocs := testing.AllocsPerRun(1000, func() {
+		sb := Get()
+		sb.WriteInt(math.MinInt64)
+		sb.WriteByte(' ')
+		sb.WriteInt(math.MaxInt64)
+		sb.WriteByte(' ')
+		sb.WriteUint(math.MaxUint64)
+		got = sb.String()
+	})
+	if got != want {
+		t.Errorf("got %q; want %q", got, want)
+	}
+	if allocs != 1 {
+		t.Errorf("allocs = %v; want 1", allocs)
+	}
+}