Как строки работают в Golang внутри
В языке Go строки (string) являются неизменяемыми последовательностями байт, закодированных в UTF-8. Это означает, что строка не представляет собой последовательность символов в привычном смысле, а хранит байты, которые могут представлять как ASCII-символы (один байт), так и многобайтовые символы Unicode.
Go стремится к простоте и безопасности, поэтому строки реализованы с фокусом на неизменяемость, производительность и удобство работы с Unicode, включая поддержку рун (rune — псевдоним для int32, представляющий символ Unicode).
1. Внутреннее представление
На уровне рантайма строка в Go представлена как структура из двух полей:
type stringStruct struct {
data \*byte // указатель на первый байт строки
len int // длина строки в байтах
}
Строка — это read-only срез байтов: она указывает на участок памяти и хранит длину, но не может быть изменена без копирования.
Пример:
s := "hello"
Строка s содержит указатель на первый байт ('h') и длину 5. В памяти: [104 101 108 108 111].
2. Иммутабельность (неизменяемость)
После создания строку нельзя изменить. Любая операция, которая выглядит как изменение строки, на самом деле создаёт новую строку.
s := "hello"
// s\[0\] = 'H' // ошибка компиляции
Чтобы изменить строку, её необходимо преобразовать в срез:
b := \[\]byte(s)
b\[0\] = 'H'
s2 := string(b) // "Hello"
Иммутабельность позволяет:
-
безопасно использовать строки в многопоточности;
-
эффективно кешировать и шарить данные;
-
избежать скрытых побочных эффектов.
3. Размер строки
Строка измеряется в байтах, а не в символах. Например:
s := "Привет"
fmt.Println(len(s)) // 12 (6 символов по 2 байта в UTF-8)
Если строка состоит только из ASCII-символов, то len(s) равен количеству символов. Но при наличии Unicode-символов длина может быть больше.
4. Символы и rune
Go использует тип rune (это просто int32) для представления одного символа Unicode.
s := "你好"
fmt.Println(len(s)) // 6 (каждый иероглиф — 3 байта)
fmt.Println(utf8.RuneCountInString(s)) // 2
Итерирование по строке с помощью for range возвращает rune и позицию:
for i, r := range s {
fmt.Printf("%d: %c\\n", i, r)
}
Это безопасно для UTF-8.
5. Преобразования между строками и байтами
string → []byte
s := "hello"
b := \[\]byte(s) // копирует байты
[]byte → string
b := \[\]byte{72, 101, 108, 108, 111}
s := string(b)
Эти операции создают новые объекты в памяти. Они важны при работе с файлами, сетевыми данными и бинарными протоколами.
6. Преобразование в rune-слайс
Для безопасной работы с символами:
s := "Привет"
r := \[\]rune(s) // \[\]rune{'П','р','и','в','е','т'}
[]rune хранит каждый символ Unicode как int32, позволяя безопасно изменять строку по символам, а не байтам.
7. Сравнение строк
Операции ==, !=, <, > и другие работают побайтово (лексикографически по UTF-8):
s1 := "apple"
s2 := "banana"
fmt.Println(s1 < s2) // true
Сравнение происходит по байтам, но этого достаточно для правильного порядка Unicode-строк в большинстве случаев.
8. Конкатенация строк
Можно объединять строки через +:
s := "Hello, " + "world"
Также можно использовать strings.Join для более эффективного объединения множества строк:
parts := \[\]string{"Go", "is", "awesome"}
joined := strings.Join(parts, " ") // "Go is awesome"
Конкатенация строк создаёт новую строку. Частое использование + в цикле неэффективно.
9. Строительство строк с strings.Builder
Для эффективной по памяти и скорости сборки больших строк:
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(", ")
builder.WriteString("world")
s := builder.String()
strings.Builder минимизирует количество аллокаций, сохраняя производительность.
10. Сравнение string и []byte
Характеристика | string | []byte |
---|---|---|
Изменяемость | Нет | Да |
--- | --- | --- |
Размер | Только длина | Длина + вместимость (cap) |
--- | --- | --- |
Представление | UTF-8 | Любые байты |
--- | --- | --- |
Используется в | JSON, шаблонах, логах | Файлы, сетевые соединения |
--- | --- | --- |
Подходит для | Текста | Бинарных данных |
--- | --- | --- |
11. Оптимизация хранения строк
Go использует оптимизированные аллокаторы, чтобы минимизировать накладные расходы на хранение строк. Иммутабельность позволяет шарить строки без копирования — например, строка, возвращаемая из json.Unmarshal, может указывать прямо на буфер с JSON-данными.
12. Работа с Unicode
Go поддерживает Unicode благодаря UTF-8. Поддерживаются:
-
unicode — проверка классов символов (буква, цифра, пунктуация и т.д.)
-
utf8 — манипуляции с кодировкой UTF-8
-
strings — стандартные функции обработки строк
Пример:
s := "å"
fmt.Println(len(s)) // 2 байта
fmt.Println(utf8.RuneCountInString(s)) // 1 rune
13. Подстроки (срезы)
Можно брать подстроки через срезы, как у слайсов:
s := "Hello, world"
sub := s\[7:\] // "world"
⚠️ Это работает по байтам, не по символам. Может обрезать символ UTF-8:
s := "Привет"
sub := s\[:5\] // ⚠️ может быть невалидной строкой
Для правильной работы с символами — используйте []rune.
14. Интернирование строк
Go не интернирует строки автоматически (в отличие от Java). Однако константы могут быть оптимизированы компилятором. При сравнении строк s1 == s2, даже если они находятся в разных местах памяти, Go сравнивает содержимое.
15. Сборка строк с помощью буферов
var buf bytes.Buffer
buf.WriteString("Go")
buf.WriteByte(' ')
buf.WriteString("Lang")
fmt.Println(buf.String()) // "Go Lang"
bytes.Buffer эффективен при работе с бинарными и текстовыми данными, но менее специализирован для строк, чем strings.Builder.
16. Особенности строк в компиляторе Go
-
Строки — не null-terminated, как в C. Они не используют \0 для завершения.
-
При передаче строк между Go и C (через cgo), необходимо вручную добавлять \0, если требуется.
-
Компилятор Go может выносить строки в readonly сегмент памяти, если они неизменны и используются как литералы.
17. Работа со строками в шаблонах, JSON и т.д.
Строки — основной способ обмена данными:
-
JSON использует map[string]interface{}.
-
HTTP-заголовки, запросы, параметры URL — всё строки.
-
В шаблонах (html/template, text/template) строки — основной носитель данных.
18. Память и эффективность
Иммутабельность и короткая структура строки (2 слова: указатель и длина) делают строки:
-
эффективными в передаче;
-
безопасными в многопоточности;
-
лёгкими в копировании (копируется только дескриптор, а не данные).
Копирование строки по значению не копирует байты, только ссылку на данные. Новая строка ссылается на тот же буфер до тех пор, пока не изменится.