Внутреннее устройство строк


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  → копия структуры, данные общие (пока не изменятся)