7 notes about strings.builder in Golang

Thuc Le
5 min readMar 24, 2018

--

A month since Go 1.10 was released, I have a little time to work with strings.Builder and take some note of it. You maybe know about them, especially if you are familiar with bytes.Buffer. So I share them and hope they are useful for you

1. Four forms of write-method

Such as bytes.Buffer, the strings.Builder supports four methods to write data to the builder:

func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)

With four forms of writing methods, developers can select the appropriate method with input data: list of bytes, a byte, a rune or a string.

2. Way to store string data

From library usage view, we call write-methods of string.Builder to write content and call String() to get the accumulated string. But how does string.Builder organize content?

The slice

The string.Builder uses an internal slice to store pieces of data. When developer call write-methods to write content, the slice will be appended internally.

3. Using strings.Builder effectively

As you known in the 2nd note, strings.Builder organizes the content based on the internal slice to organize. When you call write-methods, they append new bytes to inner-slice. If the slice’s capacity is reached, Go will allocate a new slice with different memory space and copy old slice to a new one. It will take resource to do when the slice is large or it may create the memory issue. We should try to avoid it string.Builder

Regarding slice, golang support make([]TypeOfSlice, length, capacity) to pre-define the capacity to use. It avoids to reach the max of capacity and extends.

The strings.Builder also supports a method to pre-define the capacity before using, the Grow(). When we can pre-define the capacity we assume to use, the strings.Builder avoids allocating new slice to extends capacity.

func (b *Builder) Grow(n int)

When calling Grow(), we must define a number of bytes (n) that we want extends for capacity. The Grow() method make sure the Builder will have enough n free space in inner-slice to write. The capacity extending only occurs when inner-slice’s capacity doesn’t have enough free space to write n bytes, e.g.

  • Builder’s inner slice’s capacity: 10
  • Builder’s inner slice’s length: 5
  • If we call Grow(3) => the capacity isn’t extended because the current capacity’s frees pace is 5 bytes, it is enough to handle next 3 bytes.
  • If we call Grow(7) => the capacity is extended because capacity’s free space’s only 5 bytes and can’t handle next 7 bytes.

A quick quiz with this case, if we call Grow(7), what value is the final capacity when it’s extended?

17 or 12?

Actually, it is extended to27.The Grow() method of strings.Builder increase the inner-slice’s capacity to value current_capacity * 2 + n (with n is a number of bytes that you want to extend). That’s why the extended capacity will be 10*2+7 = 27.

Another note when you assume about the capacity of strings.Builder when pre-defining. The rune and a character of string can be more than 1 bytes when you WriteRune() or WriteString(), if you know, the UTF-8

4. String()

As bytes.Buffer, strings.Builder supports String() method to get final result string. For saving memory allocation, it converts the inner-buffer bytes to string as result with pointer technique. So the String() save space and time for converting.

*(*string)(unsafe.Pointer(&bytes))

5. Do not copy

The strings.Builder doesn’t recommend to copy to use. If you try to copy the strings.Builder and try to Write to it, you will got a panic

var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF")
// illegal use of non-zero Builder copied by value

As you know, the strings.Builder bases on the internal slice to storing and manage their content. The slice, internally, consists of a pointer to the array where real data is storing.

Slice internals

When we copy the Builder, we clone the pointer of the slice but they still point to the old array. The problem will be occurs when you try to Write something to copied Builder or source Builder, the other’s content will be affects. That’s reason why strings.Builder prevent copy actions.

Just an exception with zero content builder that doesn’t Write anything yet. We can copy the zero content without any error.

var b1 strings.Builder
b2 := b1
b2.WriteString("DEF")
b1.WriteString("ABC")
// b1 = ABC, b2 = DEF

The strings.Builder checks the copy action on following methods:

Grow(n int)
Write(p []byte)
WriteRune(r rune)
WriteString(s string)

So, it’s fine if we copy and use methods:

// Reset()
// Len()
// String()
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.Len()) // 3
fmt.Println(b2.String()) // ABC
b2.Reset()
b2.WriteString("DEF")
fmt.Println(b2.String()) // DEF

6. Concurrency supporting

As bytes.Buffer, the strings.Builder doesn’t support concurrency when writing and reading. So we should take care it if we need them.

We can try a little bit with strings.Builder to add 10000 character, at the same time.

package mainimport (
"fmt"
"strings"
"sync"
)
func main() {
var b strings.Builder
var wait sync.WaitGroup
for i := 0; i < 10000; i++ {
wait.Add(1)
go func() {
b.WriteString("1")
wait.Done()
}()
}
wait.Wait()
fmt.Println(len(b.String()))
}

If you run it, you had different result’s lengths. But they aren’t enough 10000 as we add.

go run main.go => 7329
go run main.go => 7650
go run main.go => 7623

7. io.Writer interface

The io.Writer interface is implemented on strings.Builder with Write() method Write(p []byte) (n int, err error). So, we have a lot of useful case with io.Writer:

  • io.Copy(dst Writer, src Reader) (written int64, err error)
  • bufio.NewWriter(w io.Writer) *Writer
  • fmt.Fprint(w io.Writer, a …interface{}) (n int, err error)
  • func (r *http.Request) Write(w io.Writer) error
  • and other libraries that uses io.Writer

--

--