Строки в Go
Строки в Go
Строка в Go — это immutable (неизменяемая) последовательность байтов.
Важно понимать: строка — это не массив символов, а именно массив байтов. По умолчанию Go использует UTF-8 кодировку.
Внутреннее устройство
На уровне runtime строка представлена как структура из двух полей:
type stringStruct struct {
str unsafe.Pointer // указатель на массив байтов
len int // длина в байтах
}
Когда вы объявляете s := "Hello", в памяти создается:
- Массив байтов
[72 101 108 108 111](ASCII коды) - Структура
stringStruct, которая указывает на этот массив и хранит длину5
Ключевые особенности
1. Неизменяемость (Immutability)
s := "hello"
s[0] = 'H' // ОШИБКА компиляции! Нельзя изменить строку
Это фундаментальное свойство. Строки неизменяемы, что даёт:
- Безопасность: можно передавать строки между горутинами без синхронизации
- Эффективность: строки можно безопасно шарить (sharing), substring не копирует данные
- Простоту: нет проблем с concurrent modification
2. UTF-8 кодирование
Go нативно поддерживает UTF-8:
s := "Привет, мир!"
fmt.Println(len(s)) // 21 (количество БАЙТОВ, не символов!)
Почему 21? Кириллические символы в UTF-8 занимают 2 байта каждый:
"Привет"= 6 символов × 2 байта = 12 байтов", "= 2 символа × 1 байт = 2 байта"мир"= 3 символа × 2 байта = 6 байтов"!"= 1 байт- Итого: 21 байт
3. Длина vs количество символов
s := "Hello, 世界"
// Длина в байтах
fmt.Println(len(s)) // 13
// Количество рун (символов Unicode)
fmt.Println(utf8.RuneCountInString(s)) // 9
Китайские иероглифы 世界 занимают по 3 байта каждый в UTF-8.
Работа со строками
Индексация
s := "Hello"
fmt.Println(s[0]) // 72 (ASCII код 'H')
Внимание:
s[i]возвращает байт (uint8), а не символ!
Итерация
По байтам:
s := "Привет"
for i := 0; i < len(s); i++ {
fmt.Printf("%d ", s[i]) // выведет байты
}
По рунам (правильный способ для Unicode):
s := "Привет"
for i, r := range s {
fmt.Printf("позиция %d: символ %c (руна %d)\n", i, r, r)
}
range автоматически декодирует UTF-8 и возвращает руны (int32).
Substring (срезы строк)
s := "Hello, World!"
sub := s[7:12] // "World"
Важная оптимизация: substring не копирует данные! Он создает новую структуру stringStruct, которая указывает на часть того же underlying массива байтов.
s := "Hello, World!"
sub := s[0:5] // "Hello"
// sub.str указывает на тот же массив, что и s.str
// sub.len = 5
Потенциальная проблема: если вы берёте маленький substring от огромной строки и храните его долго, вся исходная строка останется в памяти.
Решение:
// Создать копию, если нужно освободить исходную строку
sub := string([]byte(s[0:5]))
Конвертация между string и []byte
string → []byte
s := "Hello"
b := []byte(s) // КОПИРОВАНИЕ данных
Это создает копию, потому что []byte изменяемый, а string — нет.
[]byte → string
b := []byte{72, 101, 108, 108, 111}
s := string(b) // КОПИРОВАНИЕ данных
Тоже копирование по той же причине.
Оптимизация компилятора: в некоторых случаях (например, при выводе в io.Writer) компилятор может избежать копирования:
fmt.Println(string(byteSlice)) // может избежать копирования
Конкатенация строк
Оператор +
s := "Hello" + " " + "World"
Проблема: каждая операция + создает новую строку и копирует данные. O(n²) для цикла!
// ПЛОХО для большого количества конкатенаций
s := ""
for i := 0; i < 1000; i++ {
s += "x" // каждый раз копирование всей строки
}
strings.Builder (правильный способ)
var builder strings.Builder
for i := 0; i < 1000; i++ {
builder.WriteString("x")
}
s := builder.String()
strings.Builder использует внутренний []byte буфер и минимизирует аллокации.
Руны (Runes)
Руна — это тип rune, алиас для int32, представляющий Unicode code point:
s := "世"
r := []rune(s) // конвертация string → []rune
fmt.Println(r[0]) // 19990 (Unicode code point для 世)
r := '世' // одинарные кавычки создают руну
fmt.Printf("%T\n", r) // int32
Пустая строка и nil
var s1 string // zero value, s1 == ""
s2 := "" // то же самое
// Строки НЕ могут быть nil!
// var s3 *string = nil // это указатель на строку, не сама строка
Сравнение строк
Строки сравниваются лексикографически по байтам:
"abc" < "abd" // true
"Abc" < "abc" // true (заглавные буквы идут раньше в ASCII/Unicode)
Сравнение происходит за O(n), но оптимизировано: сначала сравниваются длины, потом указатели (если указывают на один адрес), и только потом содержимое.
Интернирование строк (String interning)
Go автоматически переиспользует идентичные строковые литералы:
s1 := "hello"
s2 := "hello"
// s1 и s2 указывают на одну и ту же область памяти
Это работает только для литералов, не для динамически созданных строк.
Полезные пакеты
strings— основные операции со строками (Split, Join, Contains, Replace и т.д.)strconv— конвертация между строками и другими типамиunicode/utf8— работа с UTF-8 (подсчет рун, валидация)regexp— регулярные выражения
Важные источники
- Go Blog — Strings, bytes, runes and characters in Go — отличная статья от Роба Пайка, обязательна к прочтению!
- Go Specification — String types
- Research!rsc — Go Data Structures — Расс Кокс объясняет внутреннее устройство
- Effective Go — Data section
- «The Go Programming Language» (Donovan & Kernighan) — глава 3.5
Практические рекомендации
- Используйте
strings.Builderдля построения строк в циклах - Помните про UTF-8:
len(s)— это байты, не символы - Используйте
rangeдля итерации по Unicode символам - Избегайте конверсий
string ↔ []byteв горячих местах (они копируют данные) - Будьте осторожны с substring от больших строк (memory leak)