Склеиваем строки в Go: топ-3 ошибки и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как объединить срез строк в Go: топ ошибок и как их избежать

Вы когда-нибудь писали на Go и вдруг понимали: «Мне нужно склеить все эти строки в одну, но как сделать это правильно?» Этот вопрос — один из самых популярных на Stack Overflow (десятки тысяч просмотров). Казалось бы, что сложного? Но новички (и не только) умудряются наступать на одни и те же грабли: замедляют код, расходуют кучу памяти или получают неожиданные баги.

В этой статье мы разберём три типичные ошибки при объединении среза строк в Go и покажем, как их избежать. Вы научитесь выбирать правильный инструмент для каждой задачи — от простой склейки до высоконагруженных приложений. Поехали!

Почему эта тема так важна?

Работа со строками — основа любого языка программирования. В Go строки неизменяемы (immutable). Когда вы «склеиваете» две строки через оператор +, на самом деле создаётся новая строка, а старые копируются. В цикле это превращается в квадратичную сложность O(n²). А если строк много (сотни тысяч), программа может тормозить или даже упасть с out of memory.

Кроме того, Go — язык, где производительность важна. Поэтому стандартная библиотека предлагает элегантные решения. Давайте посмотрим, какие ошибки делают чаще всего.

Ошибка №1: Конкатенация в цикле с помощью +=

Как это выглядит на практике

Допустим, у вас есть срез строк:

words := []string{"Go", "is", "awesome", "but", "only", "if", "you", "use", "the", "right", "tools"}

Самый интуитивный способ — пройтись по срезу и добавлять каждое слово:

result := ""
for _, w := range words {
    result += w + " "
}
fmt.Println(result) // "Go is awesome but only if you use the right tools "

Выглядит логично? Да. Но это — бомба замедленного действия.

Почему это плохо?

  • Копирование памяти. Каждая итерация создаёт новую строку, копируя в неё все предыдущие данные. Для 1000 строк по 10 символов — это ~500 000 лишних копирований.
  • Огромное количество аллокаций. Вместо одной строки вы создаёте n строк, каждая из которых потом удаляется сборщиком мусора.
  • Нечитаемый код. Легко забыть про пробелы или разделители.

Когда это допустимо? Только если строк всего 2-3, и вы пишете одноразовый скрипт. Для продакшена — никогда.

Ошибка №2: Использование strings.Join с пустым разделителем (и непонимание его силы)

Правильный герой

Стандартная библиотека Go даёт нам strings.Join. Это идеальное решение для 90% случаев:

import "strings"

words := []string{"Go", "is", "awesome"}
result := strings.Join(words, " ")
fmt.Println(result) // "Go is awesome"

Код читается как книга: «склей words через пробел». И работает быстро — strings.Join предварительно вычисляет итоговую длину строки и выделяет память ровно один раз.

Как ошибаются?

  • Забывают про разделитель. strings.Join(words, "") — склеит всё подряд. Иногда это нужно, но часто — баг.
  • Используют strings.Join для единичной конкатенации. Не делайте так: strings.Join([]string{a, b}, ""). Просто напишите a + b.
  • Не обрабатывают пустой срез. strings.Join([]string{}, ",") вернёт пустую строку. Это нормально, но новички ждут ошибку.
// Пример, когда Join не подходит
data := []string{}
result := strings.Join(data, ",") // "" — пусто, но иногда нужно ","
// Тогда придётся делать проверку:
if len(data) > 0 {
    result = "," + strings.Join(data, ",")
}

Вывод: strings.Join — ваш друг. Используйте его всегда, когда нужно склеить срез с разделителем.

Ошибка №3: Игнорирование strings.Builder для сложной сборки

Когда Join не спасает?

Бывают ситуации, когда вы не просто склеиваете строки, а строите их динамически: добавляете условия, форматируете числа, вставляете HTML. Например, генерируете SQL-запрос или JSON.

Тут новички часто возвращаются к конкатенации в цикле. Или, что ещё хуже, используют fmt.Sprintf внутри цикла:

// Плохой пример
var result string
for i, item := range items {
    result += fmt.Sprintf("(%d, '%s')", i, item)
}

Это сочетает две беды: конкатенацию и дорогой Sprintf на каждой итерации.

Решение: strings.Builder

Начиная с Go 1.10, у нас есть strings.Builder. Он работает как буфер, который растёт по мере необходимости, но с минимальным количеством аллокаций:

import "strings"

var sb strings.Builder
for i, item := range items {
    if i > 0 {
        sb.WriteString(", ")
    }
    sb.WriteString("(")
    sb.WriteString(strconv.Itoa(i))
    sb.WriteString(", '")
    sb.WriteString(item)
    sb.WriteString("')")
}
result := sb.String()

Плюсы:

  • Память выделяется один раз (можно даже задать начальный размер через sb.Grow(n)).
  • Не создаёт промежуточных строк.
  • Работает в 5-10 раз быстрее конкатенации в цикле.

Минусы: Код становится длиннее. Но для производительности это оправдано.

Типичная ошибка с Builder

Забыть вызвать sb.Reset() или sb.Grow(). Если вы используете Builder многократно (например, в HTTP-хендлере), обязательно сбрасывайте его:

var sb strings.Builder
for _, req := range requests {
    sb.Reset()
    sb.Grow(256) // оцениваем размер
    // строим строку
    result := sb.String()
}

Сравнение производительности: бенчмарки

Давайте посмотрим на цифры. Я написал простой бенчмарк для 1000 строк по 10 символов:

Блоги

Рекомендаций по книгам

Метод Время (ns/op) Аллокаций (B/op) Кол-во аллокаций
Конкатенация (+=)