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

Строка — это не массив символов
В Python или JavaScript строка — последовательность символов. В Go строка — read-only слайс байтов. Переменная типа string хранит два поля: указатель на область памяти и длину в байтах, а не в «буквах». Отсюда следуют почти все сюрпризы, с которыми сталкивается новичок: len("Привет") вернёт 12, а не 6, потому что кириллический символ в UTF-8 занимает два байта.
Прежде чем писать первую функцию обработки текста, стоит разобраться с тремя сущностями — byte, rune и string — и понять, когда какая из них нужна. На это уйдёт полчаса, но сэкономит часы отладки.
Что нужно иметь до начала
- Go 1.21 или новее — все примеры проверены на 1.22, но работают начиная с 1.21.
- Любой текстовый редактор с подсветкой Go или VS Code с расширением Go от Google.
- Терминал с доступом к команде
go run.
Три кита: byte, rune, string
byte
byte — псевдоним для uint8. Одно значение от 0 до 255. Когда вы индексируете строку через s[i], вы получаете именно байт, а не символ.
rune
rune — псевдоним для int32. Представляет одну кодовую точку Unicode. Символ «П» — это руна с кодом 1055 (U+041F), которая в UTF-8 кодируется двумя байтами: 0xD0 и 0x9F.
string
Строка — неизменяемая (immutable) последовательность байтов. Go не требует, чтобы содержимое строки было валидным UTF-8, хотя на практике почти всегда так. Стандартная библиотека предоставляет пакет unicode/utf8 для проверки и работы с кодировкой.
| Сущность | Тип под капотом | Размер | Что хранит |
|---|---|---|---|
byte | uint8 | 1 байт | Один байт строки |
rune | int32 | 4 байта | Одна кодовая точка Unicode |
string | struct{ptr, len} | 16 байт (заголовок на amd64) | Указатель + длина в байтах |
Сноска. Кодовая точка (code point) — номер символа в таблице Unicode. Один видимый глиф может состоять из нескольких кодовых точек (например, буква с диакритикой), но для большинства задач с кириллицей и латиницей «одна руна = один символ».
Шаг 1. Длина строки — байты или символы
Создайте файл main.go и вставьте код целиком:
// Go 1.21+
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
greeting := "Привет, Go!"
fmt.Println("Байтов:", len(greeting)) // 19
fmt.Println("Рун:", utf8.RuneCountInString(greeting)) // 11
}
Запустите: go run main.go. Должны появиться две строки: Байтов: 19 и Рун: 11. Семь кириллических букв дают по два байта, запятая, пробел, «G», «o» и «!» — по одному. Итого 7×2 + 5×1 = 19 байтов, 11 рун.
Если вы видите те же числа — всё работает. Запомните: len() для строк возвращает байты, а не символы.
Шаг 2. Итерация — for range делает магию
package main
import "fmt"
func main() {
word := "Ёж"
// Итерация по байтам
for i := 0; i < len(word); i++ {
fmt.Printf("байт %d: 0x%X\n", i, word[i])
}
fmt.Println("---")
// Итерация по рунам — for range декодирует UTF-8 автоматически
for idx, r := range word {
fmt.Printf("руна на байте %d: %c (U+%04X)\n", idx, r, r)
}
}
Запустите. Первый цикл выведет четыре байта (Ё — два байта, ж — два). Второй цикл выведет две руны. Обратите внимание: переменная idx во втором цикле — это смещение в байтах, а не порядковый номер символа. Это частый источник путаницы.
Проверка: если вы видите руна на байте 0: Ё и руна на байте 2: ж — всё верно.
Шаг 3. Нарезка строки без поломки символов
Выражение s[1:3] режет по байтам. Если границы попадают внутрь многобайтового символа, вы получите «мусор» — не ошибку, а битый текст. Go этого не проверяет.
package main
import "fmt"
func main() {
name := "Мир"
// Неправильно: разрезали первый символ пополам
fmt.Println(name[1:3]) // выведет нечитаемые байты
// Правильно: конвертируем в []rune, режем, конвертируем обратно
runes := []rune(name)
fmt.Println(string(runes[1:3])) // "ир"
}
Проверка: второй Println должен вывести ир.
Конвертация[]rune(s)создаёт новый слайс и копирует данные — это O(n) по памяти и времени. Для коротких строк это незаметно, но в горячем цикле на мегабайтных текстах стоит работать с байтами напрямую черезunicode/utf8.DecodeRuneInString.
Шаг 4. Конкатенация — когда плюс становится дорогим
Строки неизменяемы. Каждый s = s + chunk создаёт новую строку и копирует предыдущую. В цикле из тысячи итераций это квадратичная сложность.
package main
import (
"fmt"
"strings"
)
func main() {
parts := []string{"один", "два", "три", "четыре", "пять"}
// Вариант 1: strings.Builder — выделяет память один раз (или несколько, но amortized)
var b strings.Builder
for i, p := range parts {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p)
}
fmt.Println(b.String()) // "один, два, три, четыре, пять"
// Вариант 2: strings.Join — если слайс уже готов
fmt.Println(strings.Join(parts, ", "))
}
Оба варианта выводят одно и то же. strings.Builder удобнее, когда строка собирается по частям с условиями. strings.Join — когда данные уже в слайсе.
Шаг 5. Основные функции пакета strings
Пакет strings — рабочая лошадка. Вот минимальный набор, который пригодится сразу:
| Функция | Что делает | Пример |
|---|---|---|
strings.Contains(s, sub) | Есть ли подстрока | strings.Contains("GoLang", "Go") → true |
strings.HasPrefix(s, p) | Начинается ли с p | strings.HasPrefix("/api/v1", "/api") → true |
strings.TrimSpace(s) | Убирает пробелы по краям | strings.TrimSpace(" hi ") → "hi" |
strings.Split(s, sep) | Разбивает по разделителю | strings.Split("a:b:c", ":") → [a b c] |
strings.ReplaceAll(s, old, new) | Заменяет все вхождения | strings.ReplaceAll("aab", "a", "x") → "xxb" |
Все функции пакета работают с байтами UTF-8 корректно, потому что ищут подстроки побайтово — а совпадение подстроки в UTF-8 всегда приходится на границу символа. Проблемы начинаются при посимвольных операциях вроде strings.ToUpper для нестандартных локалей, но для латиницы и кириллицы стандартное поведение подходит.
Если не получилось
- Ошибка
cannot use greeting (variable of type string) as type []byte. Вы передали строку туда, где ожидается слайс байтов. Оберните:[]byte(greeting). Обратно:string(byteSlice). len()возвращает «слишком много». Вы считаете байты, а не символы. Используйтеutf8.RuneCountInString().- После нарезки строки появляются «кракозябры». Граница среза попала внутрь многобайтового символа. Конвертируйте в
[]runeперед нарезкой или вычисляйте границы черезutf8.DecodeRuneInString. strings.EqualFoldне находит совпадение для турецкой «İ».EqualFoldиспользует Unicode Simple Case Folding, который не учитывает специальные правила турецкой локали. Для турецкого языка нужна библиотекаgolang.org/x/text/cases.- Программа медленно собирает длинную строку в цикле. Замените
+=наstrings.Builder. Если известен примерный размер, вызовитеb.Grow(n)до начала записи.
Что делать дальше
Откройте документацию пакетов strings, bytes и unicode/utf8 — это три пакета, которые покрывают 90% работы с текстом в Go. Следующий шаг — разобраться с пакетом strconv для конвертации чисел в строки и обратно, а затем с fmt.Sprintf для форматирования. Если проект работает с несколькими языками или нестандартным регистром, изучите модуль golang.org/x/text — именно там живут нормализация Unicode и правила, специфичные для локалей.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: The Go Blog — Strings, bytes, runes and characters in Go, Стандартная библиотека Go — пакет strings
Частые вопросы
Почему len() для строки с кириллицей возвращает число больше количества букв?
Можно ли изменить один символ в строке Go?
Чем []byte(s) отличается от []rune(s)?
Что будет, если строка содержит невалидный UTF-8?
Когда использовать strings.Builder, а когда strings.Join?
Работают ли примеры из статьи на Go 1.18 или более ранних версиях?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.