Конкатенация строк


1. Базовый синтаксис

Оператор +

hello := "Hello"
world := "World"
greeting := hello + ", " + world + "!"

fmt.Println(greeting)  // "Hello, World!"

Работает интуитивно — складываем строки как числа.


Сокращённая форма +=

message := "Go"
message += " is"
message += " awesome!"

fmt.Println(message)  // "Go is awesome!"

Аналог: message = message + " is".


2. Проблема: сколько строк создаётся?

Вопрос из материала

greeting := hello + ", " + world + "!"

Кажется: 3 строки (hello, world, greeting).

На самом деле: 6 строк!


Детальный разбор

hello := "Hello"          // 1. Строка "Hello"
world := "World"          // 2. Строка "World"
greeting := hello + ", " + world + "!"

Пошаговое выполнение:

Шаг Операция Результат Строка в памяти
1 hello "Hello" Строка #1
2 world "World" Строка #2
3 ", " ", " Строка #3 (константа)
4 hello + ", " "Hello, " Строка #4 (временная)
5 ... + world "Hello, World" Строка #5 (временная)
6 "!" "!" Строка #6 (константа)
7 ... + "!" "Hello, World!" Строка #7 (финальная)

Итого: 7 строк (включая константы и временные).


3. Почему это дорого

Механизм конкатенации

При каждом + происходит:

  1. Выделение новой памяти под результат
  2. Копирование байтов левой строки
  3. Копирование байтов правой строки
  4. Создание новой структуры StringHeader
"Hello" + ", " + "World"
  ↓
Выделить память (8 байт)
Скопировать "Hello" (5 байт)
Скопировать ", " (2 байт)
→ "Hello, " (временная строка)
  ↓
Выделить память (12 байт)
Скопировать "Hello, " (7 байт)
Скопировать "World" (5 байт)
→ "Hello, World"

Проблема: Каждая промежуточная строка создаётся в памяти.


4. Когда конкатенация приемлема

Небольшое количество операций

name := "Alice"
greeting := "Hello, " + name + "!"  // ✅ OK

Допустимо: 2-3 операции +, редкие вызовы.


5. Когда конкатенация проблематична

Циклы и большие объёмы

// ❌ ПЛОХО: Количество операций стремится к n²
result := ""
for i := 0; i < 1000000; i++ {
    result += "a"  // Каждый раз новая строка!
}

Почему плохо:

  • 1-я итерация: выделить 1 байт
  • 2-я итерация: выделить 2 байта, скопировать 1
  • 3-я итерация: выделить 3 байта, скопировать 2
  • n-я итерация: выделить n байт, скопировать n-1

6. Альтернативы (предварительно)

Из материала:
“Нужно использовать какие-то другие механизмы и инструменты”.

Для циклов: strings.Builder

var builder strings.Builder

for i := 0; i < 1000000; i++ {
    builder.WriteString("a")
}

result := builder.String()

Преимущество: память выделяется один раз с запасом.


Для известного количества: strings.Join

parts := []string{"Hello", "World", "!"}
result := strings.Join(parts, ", ")

fmt.Println(result)  // "Hello, World, !"

7. Примеры неэффективности

Плохо: цикл с +=

s := ""
for i := 0; i < 10000; i++ {
    s += fmt.Sprintf("Item %d\n", i)  // ❌ Каждый раз новая строка
}

Хорошо: strings.Builder

var builder strings.Builder
for i := 0; i < 10000; i++ {
    builder.WriteString(fmt.Sprintf("Item %d\n", i))
}
s := builder.String()

8. Сравнение производительности

Метод 1000 строк 100,000 строк
+= ~1ms ~5000ms
strings.Builder ~0.5ms ~10ms

Разница: На больших объёмах — в сотни раз.


9. Правило большого пальца

Используйте +:

  • Однократная конкатенация
  • 2-3 операции
  • Статические строки

Не используйте +:

  • Циклы
  • Сотни/тысячи операций
  • Динамическое построение

10. Итоги

  • ✅ Оператор + работает для строк
  • += — сокращённая форма
  • ❌ Каждый + создаёт новую строку в памяти
  • ❌ В выражении a + b + c + d создаётся много временных строк
  • ❌ Конкатенация в циклах — много операций
  • ✅ Для больших объёмов — strings.Builder или strings.Join
  • ✅ На малых объёмах + приемлем

Ключевое правило:

Простая конкатенация: a + b + c     → OK для 2-3 строк
Цикл с конкатенацией: s += x        → ❌ Используйте Builder
Сотни тысяч операций: builder.Write → очень быстрая работа

Практический совет:

// Плохо для больших N
for i := 0; i < N; i++ {
    result += str  // медленно
}

// Хорошо для любых N
var b strings.Builder
for i := 0; i < N; i++ {
    b.WriteString(str)  // быстро
}
result := b.String()