Эффективная конкатенация строк в Go: как правильно и быстро соединять строки
Правильный ответ: используйте strings.Builder
В современных версиях Go (1.10+) лучший способ конкатенации строк в цикле — это strings.Builder. Он специально спроектирован для эффективного построения строк и минимизирует количество аллокаций памяти.
Вот как переписать ваш пример:
package main
import (
"fmt"
"strings"
)
func main() {
var sb strings.Builder
// Предварительное выделение памяти ускоряет работу
sb.Grow(1000000) // зарезервируем ~1 МБ
for i := 0; i < 100000; i++ {
sb.WriteString(fmt.Sprintf("<div>%d</div>", i))
}
result := sb.String()
fmt.Println(len(result))
}
Почему это работает быстрее?
- Внутренний буфер:
strings.Builderхранит данные в изменяемом срезе байтов ([]byte), который растёт по мере необходимости (обычно удваивая ёмкость). - Минимум копирований: В отличие от оператора
+, который каждый раз копирует всю старую строку, Builder просто дописывает новые данные в конец буфера. - Метод
Grow(): Если вы знаете примерный размер итоговой строки, вызовитеsb.Grow(n)— это выделит память один раз, а не несколько раз при расширении буфера. - Финальный
String(): При вызове.String()создаётся новая строка, но это единственное копирование данных за всё время работы.
Сравнение с другими подходами
| Метод | Скорость | Память | Когда использовать |
|---|---|---|---|
strings.Builder | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Всегда в циклах и динамической сборке |
bytes.Buffer | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Когда нужен io.Writer или работа с байтами |
strings.Join() | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Когда все части известны заранее (срез строк) |
+ оператор | ⭐ | ⭐ | Только для 2-3 коротких строк |
Бенчмарк для наглядности
func BenchmarkPlus(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s += "a"
}
_ = s
}
func BenchmarkBuilder(b *testing.B) {
var sb strings.Builder
for i := 0; i < b.N; i++ {
sb.WriteString("a")
}
_ = sb.String()
}
При b.N = 100000 оператор + будет в 1000+ раз медленнее и создаст в сотни раз больше мусора для GC.
Важные нюансы
- Не копируйте Builder:
strings.Builderне должен копироваться после начала записи (используйте указатель*strings.Builder). - Для форматирования: Если вам нужно вставлять числа/переменные, используйте
fmt.Fprintf(&sb, "...", args)вместоsb.WriteString(fmt.Sprintf(...))— это избежит лишней аллокации промежуточной строки. - Альтернатива для IO: Если вы пишете в сеть или файл, рассмотрите
bufio.Writerповерхstrings.Builder.
Итог: всегда используйте strings.Builder для конкатенации строк в циклах — это идиоматичный, быстрый и экономичный способ в Go.
Альтернатива: bytes.Buffer и ручная работа со срезом байтов
До появления strings.Builder (Go 1.10) стандартным решением был bytes.Buffer. Он всё ещё полезен, если вам нужно не только собрать строку, но и сразу записать её в файл или сетевой сокет, так как bytes.Buffer реализует интерфейс io.Writer.
var buf bytes.Buffer
for i := 0; i < 100000; i++ {
fmt.Fprintf(&buf, "<div>%d</div>", i)
}
result := buf.String()
Отличия от strings.Builder:
bytes.Bufferхранит данные как[]byte, но при вызове.String()он возвращает копию (а не разделяет память, как Builder).- Builder чуть быстрее за счёт отсутствия лишних проверок (например, на чтение из пустого буфера).
- Если вам нужно динамически вставлять строки в середину или удалять части,
bytes.Bufferимеет методыTruncate()иReset().
Экстремальный случай — ручное управление срезом:
data := make([]byte, 0, 1000000)
for i := 0; i < 100000; i++ {
data = append(data, fmt.Sprintf("<div>%d</div>", i)...)
}
result := string(data)
Этот подход даёт максимальный контроль, но требует ручного управления ёмкостью и неудобен при сложном форматировании. Используйте его только если strings.Builder является узким местом по производительности (что бывает крайне редко).
Вывод: для 99% задач выбирайте strings.Builder, для IO-операций — bytes.Buffer, а ручное управление оставьте для низкоуровневой оптимизации.