Go

How do you build strings efficiently? Compare +, fmt.Sprintf, bytes.Buffer and strings.Builder.

Question 69MediumGo 1.22 to 1.25

Because strings are immutable, s += x in a loop allocates a new string and copies everything built so far each time, which is O(n²). The options compare like this:

  • + works for a fixed, small number of pieces. The compiler merges a + b + c into one allocation.
  • fmt.Sprintf is convenient but slow: it uses reflection and boxes arguments in interfaces.
  • strings.Join is best when you already have a []string. It computes the total size and allocates once.
  • strings.Builder is best for building incrementally. Its String() method returns the internal []byte as a string without copying, using unsafe.String. That is safe because the Builder only ever appends.
  • bytes.Buffer is a read/write buffer that implements io.Reader. Its String() copies.
func join(parts []string) string {
	var b strings.Builder
	n := 0
	for _, p := range parts {
		n += len(p) + 1
	}
	b.Grow(n) // single allocation
	for i, p := range parts {
		if i > 0 {
			b.WriteByte(',')
		}
		b.WriteString(p)
	}
	return b.String()
}

// Gotcha: copying a non-zero Builder panics on the next write
var b1 strings.Builder
b1.WriteString("x")
b2 := b1 // copy
// b2.WriteString("y") // panic: strings: illegal use of non-zero Builder copied by value
_ = b2

A Builder stores a pointer to itself (addr) to detect copies. A copy would share the byte slice, and appends through two headers would break the zero-copy guarantee. Always pass *strings.Builder. Also use strconv.AppendInt or fmt.Fprintf(&b, ...) rather than Sprintf followed by WriteString.

More on Arrays, Slices, Maps & Strings

All 37 Arrays, Slices, Maps & Strings questions