What does converting between string and []byte cost, and how do you build strings efficiently?
Question 13HardGo 1.22 to 1.25
Converting []byte(s) or string(b) normally allocates and copies the data. This is required because strings are immutable and byte slices are not. The compiler skips the copy in several common cases:
- map lookups with
m[string(b)] - comparisons like
string(b) == "x" - concatenation operands
for range []byte(s)- small conversions that don't escape (these use a stack buffer)
Repeated s += x in a loop is O(n²), because every step copies the whole string built so far. Use strings.Builder instead. Its String() method returns the internal buffer without copying.
func join(parts []string) string {
var b strings.Builder
n := 0
for _, p := range parts {
n += len(p)
}
b.Grow(n) // one allocation
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
// Zero-copy conversion (Go 1.20+). Only safe if the bytes are never modified afterwards:
func bytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
Gotchas:
- A
strings.Buildermust not be copied after its first use. Writing to such a copy panics, because the Builder records its own address and checks it on every write. bytes.Buffercan also read, so it's the right choice for I/O.strings.Builderis leaner for building a string.- For a small, fixed number of pieces,
+orfmt.Sprintfis fine. Measure with a benchmark before you optimize.
More on Language Fundamentals & Types
- Q11How are strings represented in Go? What does this print?
- Q12What happens when you range over invalid UTF-8, and what does
string(65)produce? - Q14When are the arguments to a deferred call evaluated? What does this print?
- Q15In what order do deferred calls run, and what is wrong with using
deferinside a loop? - Q16How can a deferred function change a function's return value? What do
f()andg()return? - Q17What does this print? (defer with value vs pointer receivers)