Внутреннее устройство строк
1. Структура строки в памяти
Строка в Go — это структура из двух полей:
type StringHeader struct {
Data uintptr // Указатель на массив байтов
Len int // Длина строки в байтах
}
Визуализация:
s := "Go"
┌──────────────────────┐
│ StringHeader (16б) │
├──────────────────────┤
│ Data: 0x123456 (8б) │─────┐
│ Len: 2 (8б) │ │
└──────────────────────┘ ↓
┌─────────┐
│ G │ o │
└─────────┘
Ключевое: Строка хранит указатель на данные, а не сами данные.
2. Копирование строк
Поведение при присваивании
s1 := "Go"
s2 := s1 // Копируется структура (Data + Len), не данные!
fmt.Printf("s1 адрес: %p\n", &s1) // 0xc0000a0000
fmt.Printf("s2 адрес: %p\n", &s2) // 0xc0000a0008 — РАЗНЫЕ адреса
Вывод: Переменные s1 и s2 находятся в разных местах памяти.
Адреса данных (указатель Data)
s1 := "Go"
s2 := s1
data1 := unsafe.StringData(s1)
data2 := unsafe.StringData(s2)
fmt.Printf("s1 данные: %p\n", data1) // 0x10a4c80
fmt.Printf("s2 данные: %p\n", data2) // 0x10a4c80 — ОДИНАКОВЫЕ!
Вывод: Обе строки указывают на один и тот же массив байтов.
3. Визуализация копирования
Память:
┌─────────────┐
│ s1 (16 байт)│ → Data: 0x10a4c80, Len: 2
└─────────────┘
┌─────────────┐
│ s2 (16 байт)│ → Data: 0x10a4c80, Len: 2 (тот же Data!)
└─────────────┘
↓
┌──────────┐
│ G │ o │ (общие данные)
└──────────┘
Механизм:
&s1≠&s2— переменные в разных адресахDataодинаковый — указывают на одни данные
4. Оптимизация компилятора
Строковые литералы
s1 := "Go"
s2 := "Go" // Без присваивания!
data1 := unsafe.StringData(s1)
data2 := unsafe.StringData(s2)
fmt.Printf("s1 данные: %p\n", data1) // 0x10a4c80
fmt.Printf("s2 данные: %p\n", data2) // 0x10a4c80 — ОДИНАКОВЫЕ
Оптимизация: Компилятор может переиспользовать одинаковые строковые литералы.
Но не всегда!
var s1 string
fmt.Scanln(&s1) // Вводим "Go"
s2 := "Go"
data1 := unsafe.StringData(s1)
data2 := unsafe.StringData(s2)
fmt.Printf("s1 данные: %p\n", data1) // 0xc000010200
fmt.Printf("s2 данные: %p\n", data2) // 0x10a4c80 — РАЗНЫЕ!
Причина: Строка из Scanln создаётся динамически, литерал "Go" — статически.
5. Когда строки разделяют данные
| Ситуация | Общие данные? |
|---|---|
s2 := s1 |
✅ Да |
s1 := "Go"; s2 := "Go" |
✅ Да (оптимизация) |
fmt.Scanln(&s1); s2 := s1 |
✅ Да |
fmt.Scanln(&s1); s2 := "Go" |
❌ Нет (разные источники) |
Важно: Нельзя гарантировать, что одинаковые строки всегда будут в одном месте.
6. Неизменяемость (immutability)
s := "Go"
// s[0] = 'X' // ❌ Ошибка компиляции: cannot assign to s[0]
Причина: Массив байтов, на который указывает Data, расположен в read-only памяти.
Обход через преобразование
s := "Go"
// Преобразуем в []byte
bytes := []byte(s)
bytes[0] = 'N'
newS := string(bytes)
fmt.Println(newS) // "No"
fmt.Println(s) // "Go" — оригинал не изменён
7. Практический пример: проверка общих данных
s1 := "Hello"
s2 := s1
s3 := "Hello"
data1 := unsafe.StringData(s1)
data2 := unsafe.StringData(s2)
data3 := unsafe.StringData(s3)
fmt.Printf("s1: %p\n", data1)
fmt.Printf("s2: %p\n", data2)
fmt.Printf("s3: %p\n", data3)
if data1 == data2 {
fmt.Println("✅ s1 и s2 — общие данные")
}
if data1 == data3 {
fmt.Println("✅ s1 и s3 — общие данные (оптимизация)")
}
Возможный вывод:
s1: 0x10a4c90
s2: 0x10a4c90
s3: 0x10a4c90
✅ s1 и s2 — общие данные
✅ s1 и s3 — общие данные (оптимизация)
8. Размер структуры
s := "Go"
fmt.Println(unsafe.Sizeof(s)) // 16 байт (на 64-битной системе)
Почему 16 байт?
Data (uintptr): 8 байт
Len (int): 8 байт
──────────────────────
Итого: 16 байт
9. Отличие от других языков
| Язык | Внутреннее устройство |
|---|---|
| Go | (Data *byte, Len int) |
| Python | Объект с данными + длина + хеш-кэш |
| Java | char[] + offset + length |
| C | Просто char* (массив с \0) |
Особенность Go: Строка знает свою длину без сканирования.
10. Итоги
✅ Строка = структура: указатель Data + длина Len
✅ При присваивании копируется структура, не данные
✅ Переменные в разных адресах, но данные могут быть общими
✅ Компилятор оптимизирует одинаковые литералы
✅ Динамические строки (из Scanln) имеют отдельные данные
✅ Строки иммутабельны — нельзя изменить s[i]
✅ Размер структуры: 16 байт (на 64-бит системах)
Ключевое правило:
Одинаковые строки НЕ ВСЕГДА разделяют данные
Проверяйте через unsafe.StringData() при необходимости
Практический вывод:
s2 := s1 → копия структуры, данные общие (пока не изменятся)