Изменить строку: []byte и Builder
После инцидента Сергей попросил Мишу почистить сборку путей к файлам. Склейка строк на горячем пути тормозила заметно, и Миша переписал её без промежуточных копий. Аня проверила правку на том же загрузочном тесте — стало быстрее.
чтобы «изменить» неизменяемую строку, её копируют через []byte или собирают в strings.Builder — оба пути дают новую строку, но Builder делает это одним финальным преобразованием без копии.
Рано или поздно строку нужно изменить, а неизменяемость этому мешает. Стандартный обходной путь — сходить через слайс байтов. Преобразование string → []byte и обратно копирует данные, поэтому изменение слайса не задевает исходную строку.
Правка через []byte
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
Копирование здесь обязательно: строка неизменяема, слайс — изменяем, и если бы они делили один массив, слайс мог бы нарушить неизменяемость строки. Спецификация требует независимой семантики значений — изменяемый слайс не может делить данные с неизменяемой строкой. Поэтому преобразование даёт независимое значение: обычно это физическая копия.
Отсюда типичный паттерн, когда строку нужно «исправить»: преобразовать в []byte, изменить байты, преобразовать обратно.
func asciiUpper(s string) string {
b := []byte(s)
for i := range b {
if b[i] >= 'a' && b[i] <= 'z' {
b[i] -= 'a' - 'A'
}
}
return string(b)
}
Слово ascii в названии — честное предупреждение о границе метода: функция делает заглавными латинские буквы, а кириллицу оставляет как есть, потому что работает побайтово и не знает про многобайтовые символы.
Сборка по кускам через Builder
Преобразования копируют данные, а значит, в горячем цикле каждое string(b) или []byte(s) — это лишняя аллокация. Если строка собирается по частям, strings.Builder накапливает байты в своём буфере и делает одно преобразование в конце, а не по одному на каждый кусок:
var b strings.Builder
for _, part := range parts {
b.WriteString(part)
}
result := b.String() // одно преобразование вместо len(parts)
Есть и частный случай, когда компилятор выбрасывает копию сам: если результат []byte(s) нужен только для чтения и нигде не сохраняется, копия может не создаваться. Это оптимизация компилятора, опираться на неё при проектировании не стоит.
Вы узнаете:
- как
strings.Joinреализует Builder-паттерн - как проверить оптимизацию
m[string(b)]бенчмарком
Проверьте себя
Почему преобразование string(b) обязано копировать данные?
В каком случае компилятор может не копировать при []byte(s) → string(b)?
Можно ли опираться на оптимизацию m[string(b)] при проектировании?
правильных ответов
Строку не меняют — создают новую: либо через копию в []byte, либо через strings.Builder, который накапливает куски и преобразует один раз в конце — финальный String() без копии.
10 000 ₽ · 60 минут · онлайн