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.
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
: