Как строки работают в Golang внутри
Строки в Go — это неизменяемые последовательности байт, которые внутри представлены структурой из указателя на массив байтов и длины. Понимание внутреннего устройства строк помогает писать эффективный код, особенно при работе с текстом в кодировке UTF-8.
Как это работает
Внутренне строка в Go описывается структурой stringHeader (в рантайме), которая содержит два поля:
Data— указатель на первый байт массива, где хранятся данные строки.Len— длина строки в байтах, а не в символах (рунах).
Эта структура аналогична срезу (slice), но без поля вместимости (cap). Благодаря этому строки можно передавать по значению без копирования данных — копируется только заголовок (16 байт на 64-битной платформе).
Строки неизменяемы: любая операция, которая выглядит как изменение, на самом деле создаёт новую строку. Это свойство обеспечивает безопасность при передаче строк между горутинами без синхронизации, так как никто не может изменить содержимое строки из-под другого потока.
UTF-8 и длина строки
Go использует UTF-8 как стандартную кодировку для строк. Это означает, что один символ (руна) может занимать от 1 до 4 байт. Поэтому встроенная функция len(s) возвращает количество байт, а не символов. Для подсчёта рун нужно использовать utf8.RuneCountInString(s) или len([]rune(s)).
При индексации строки s[i] вы получаете байт, а не символ. Для доступа к рунам используйте range или преобразование в []rune.
Срезы строк
Срез строки s[a:b] создаёт новую строку, которая указывает на тот же массив байтов, что и исходная, но с изменёнными указателем и длиной. Это очень эффективно: не происходит копирования данных, только создание нового заголовка. Например:
s := "Hello, world"
t := s[7:] // t указывает на ту же память, но начинается с 7-го байта
fmt.Println(s) // Hello, world
fmt.Println(t) // worldОднако важно помнить, что срез строки удерживает всю исходную строку в памяти, даже если используется только её часть. Если нужно освободить память, следует скопировать подстроку в новую строку: string([]byte(s[a:b])).
Подводные камни и производительность
- Частое создание новых строк в цикле (например, через конкатенацию
+) приводит к множественным выделениям памяти и росту нагрузки на сборщик мусора. Для построения больших строк используйтеstrings.Builder. - Преобразование
stringв[]byteи обратно копирует данные, если строка не является результатом среза. Это может быть дорого для больших строк. - Сравнение строк происходит по байтам, что быстро, но для корректного сравнения с учётом Unicode (например, нормализации) нужно использовать специальные функции.
- Строки могут быть пустыми (
""), и это допустимо. Нулевой указатель вDataне означает пустую строку, но на практике пустая строка имеетLen = 0.
Коротко
- Строка — это неизменяемый срез байтов, представленный указателем и длиной.
len(s)возвращает длину в байтах, а не в символах; для рун используйтеutf8.RuneCountInString.- Срезы строк эффективны, но удерживают память исходной строки.
- Для конкатенации в цикле используйте
strings.Builder, чтобы избежать лишних аллокаций.
