Работа со строками в Go: частые ошибки новичков
Строки в Go выглядят привычно, но устроены иначе, чем в Python или JavaScript. Разберём конкретные ловушки — от подсчёта символов до неожиданного расхода памяти при конкатенации — и покажем, как их обойти.

Что не так со строками в Go
Новичок, пришедший из Python или JS, ожидает, что len(s) вернёт количество символов, что строку можно менять на месте, а конкатенация в цикле — обычное дело. В Go каждое из этих предположений ведёт к багу или к просадке производительности. Строка в Go — это неизменяемая последовательность байтов, а не символов. Это одна фраза, но из неё вытекают почти все ошибки ниже.
Материал написан для Go 1.21+ и пакетов стандартной библиотеки. Всё, что нужно до начала — установленный Go и любой редактор.
Ошибка 1. len() возвращает не то, что ожидается
Функция len для строки возвращает число байтов, а не число видимых символов. Пока текст в ASCII, разницы нет. Как только появляется кириллица, иероглифы или эмодзи, числа расходятся.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Привет"
fmt.Println(len(s)) // 12 — байты (кириллица: 2 байта на символ)
fmt.Println(utf8.RuneCountInString(s)) // 6 — руны (символы Unicode)
}
Если вы валидируете длину пароля или обрезаете заголовок по лимиту символов, используйте utf8.RuneCountInString или преобразуйте строку в []rune.
Сноска. Руна (rune) в Go — это алиас для int32, представляющий одну кодовую точку Unicode. Один видимый символ — это одна или несколько рун, но для большинства европейских языков соответствие один к одному.
Ошибка 2. Итерация по индексу вместо range
Цикл for i := 0; i < len(s); i++ проходит по байтам. Если строка содержит многобайтовые символы, вы получите отдельные байты, а не символы.
package main
import "fmt"
func main() {
s := "Го"
// Побайтовый проход — два «мусорных» значения на каждый символ
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %x\n", i, s[i])
}
// 0: d0 1: 93 2: d0 3: be
// Правильный проход — range декодирует руны автоматически
for i, r := range s {
fmt.Printf("%d: %c\n", i, r)
}
// 0: Г 2: о
}
Обратите внимание: индекс i во втором цикле прыгает с 0 на 2, потому что буква «Г» занимает два байта. Это не баг, а штатное поведение range над строкой.
Ошибка 3. Конкатенация в цикле через +
Строки в Go неизменяемы. Каждый s += chunk создаёт новую строку и копирует в неё всё предыдущее содержимое. На десяти итерациях это незаметно. На десяти тысячах — O(n²) по памяти и времени.
package main
import (
"fmt"
"strings"
)
func main() {
parts := []string{"один", "два", "три", "четыре", "пять"}
// Плохо: каждая итерация — аллокация
result := ""
for _, p := range parts {
result += p + " "
}
// Хорошо: strings.Builder выделяет буфер один раз
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
b.WriteByte(' ')
}
fmt.Println(b.String())
}
Если слайс уже готов и нужен результат через разделитель, ещё проще — strings.Join(parts, " ").
Правило большого пальца: если конкатенация внутри цикла — беритеstrings.Builder. Если склеиваете два-три фрагмента вне цикла, оператор+читаемее и не хуже по производительности.
Ошибка 4. Подстрока держит весь исходный массив
Срез строки s[a:b] не копирует данные — он ссылается на тот же массив байтов. Если вы прочитали из файла мегабайт текста и сохранили маленькую подстроку, весь мегабайт останется в памяти до тех пор, пока подстрока жива.
package main
import (
"fmt"
"strings"
)
func main() {
huge := strings.Repeat("x", 1_000_000) // 1 МБ
// Плохо: tiny ссылается на весь huge
tiny := huge[:5]
// Хорошо: принудительная копия
safe := strings.Clone(huge[:5]) // Go 1.20+
fmt.Println(tiny, safe)
}
Функция strings.Clone появилась в Go 1.20. Для более ранних версий то же делает string([]byte(huge[:5])).
Ошибка 5. Сравнение без учёта регистра через ToLower
Типичный паттерн: strings.ToLower(a) == strings.ToLower(b). Он работает, но создаёт две новые строки на каждое сравнение. Стандартная библиотека предлагает strings.EqualFold — та же семантика, ноль аллокаций.
// Медленнее: две аллокации
if strings.ToLower(email) == strings.ToLower(input) { ... }
// Быстрее: без аллокаций
if strings.EqualFold(email, input) { ... }
Разница становится ощутимой при обработке HTTP-заголовков или поиска по массиву строк.
Ошибка 6. Преобразование []byte ↔ string в горячем пути
Каждое преобразование string(byteSlice) и []byte(str) — копирование данных. Если вы читаете из сети, парсите и складываете в строку в горячем цикле, аллокации растут линейно. Где возможно, работайте с []byte до последнего момента и конвертируйте один раз.
Сводная таблица ошибок
| Ошибка | Что происходит | Чем лечить |
|---|---|---|
len(s) для Unicode | Возвращает байты, не символы | utf8.RuneCountInString / []rune |
| Итерация по индексу | Читаете отдельные байты | for _, r := range s |
s += x в цикле | O(n²) аллокации | strings.Builder / strings.Join |
| Подстрока от большой строки | Утечка памяти | strings.Clone (Go 1.20+) |
ToLower для сравнения | Лишние аллокации | strings.EqualFold |
[]byte ↔ string в цикле | Копирование на каждый вызов | Работать с []byte до финального результата |
Если не получилось
Три ситуации, с которыми чаще всего приходят на форумы:
- «Руны выводят кракозябры» — убедитесь, что исходный файл сохранён в UTF-8. Go ожидает UTF-8 по умолчанию; если данные в Windows-1251, сначала перекодируйте через пакет
golang.org/x/text/encoding/charmap. - «strings.Clone не найден» — функция доступна с Go 1.20. Проверьте версию:
go version. Для более ранних версий используйтеstring([]byte(s)). - «Builder.String() возвращает пустую строку» — обычно причина в том, что
Builderобъявлен внутри функции, а записи идут в другую копию.strings.Builderнельзя копировать после первого вызова метода записи; передавайте по указателю.
Что делать дальше
Разобравшись с базовыми ловушками, стоит заглянуть в два направления. Первое — пакет unicode/utf8: функции DecodeRuneInString, ValidString и другие пригодятся при валидации пользовательского ввода. Второе — бенчмарки: напишите func BenchmarkConcat и func BenchmarkBuilder, запустите go test -bench=. и посмотрите, насколько отличаются числа на ваших объёмах данных. Собственные замеры убеждают лучше любой статьи.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: Go Blog — Strings, bytes, runes and characters in Go, Пакет strings — стандартная библиотека Go
Частые вопросы
Чем rune отличается от byte в Go?
Можно ли менять символ в строке по индексу?
Когда конкатенация через + допустима?
А если у меня версия Go ниже 1.20 и нет strings.Clone?
Как убедиться, что строка содержит валидный UTF-8?
Что делать, если данные приходят не в UTF-8?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.