Изменяемость переменных и особенности строк в памяти

Краткое описание

В этом уроке углубляемся в работу переменных в памяти: изучаем, что происходит при изменении значения, почему адрес переменной остаётся прежним, и разбираем особое поведение строк. Узнаём, чем строки отличаются от других типов при хранении в памяти.

Ключевые концепции

Четыре составляющие переменной

Каждая переменная характеризуется четырьмя свойствами:

  1. Имя — идентификатор для обращения в коде
  2. Тип — какой тип данных хранится
  3. Значение — текущие данные
  4. Адрес — место в оперативной памяти
var age int = 35

// Имя: age
// Тип: int
// Значение: 35
// Адрес: 0x14000010108 (пример)

Изменение значения не меняет адрес

Когда вы изменяете значение переменной, новое значение записывается в ту же самую область памяти. Адрес остаётся неизменным:

var age int = 35
fmt.Printf("Адрес: %p\n", &age)  // 0x14000010108

age = 40
fmt.Printf("Адрес: %p\n", &age)  // 0x14000010108 (тот же!)

Что происходит:

  • Выделяется область памяти при объявлении
  • Записывается значение 35
  • При присваивании age = 40 старое значение перезаписывается новым
  • Адрес области памяти остаётся прежним

Разные переменные — разные адреса

Каждая новая переменная получает свой собственный участок памяти:

var age int = 40
var temperature int = 25

fmt.Printf("age адрес: %p\n", &age)              // 0x14000010108
fmt.Printf("temperature адрес: %p\n", &temperature)  // 0x14000010110

// Адреса различаются - это разные области памяти

Даже если переменные одного типа и с одинаковыми значениями — каждая занимает своё место в памяти.

Особенности строк в памяти

Строки — специфичный тип

Строки ведут себя иначе, чем простые типы (int, float, bool). Это связано с их внутренним устройством во всех языках программирования.

Структура строки в Go:

  • Сама переменная типа string занимает 16 байт
  • Эти 16 байт хранят не сами символы, а метаданные:
    • Указатель на данные (8 байт на 64-битной системе)
    • Длину строки (8 байт)

Два адреса у строки

Для строковой переменной можно получить два разных адреса:

1. Адрес самой переменной (дескриптора):

var text string = "Привет, Go!"
fmt.Printf("Адрес переменной: %p\n", &text)

Это адрес структуры (дескриптора), которая описывает строку.

2. Адрес данных строки:

fmt.Printf("Адрес данных: %p\n", unsafe.StringData(text))

Это адрес реального массива байт, где хранятся символы.

Что происходит при изменении строки

var text string = "Привет, Go!"
fmt.Printf("Адрес переменной: %p\n", &text)          // 0x1400000e018
fmt.Printf("Адрес данных: %p\n", unsafe.StringData(text))  // 0x100f18e60

text = "Привет, C#!"
fmt.Printf("Адрес переменной: %p\n", &text)          // 0x1400000e018 (не изменился!)
fmt.Printf("Адрес данных: %p\n", unsafe.StringData(text))  // 0x100f18e80 (изменился!)

Анализ:

  • Адрес переменной остаётся прежним — дескриптор в той же области памяти
  • Адрес данных изменился — новая строка создана в другом месте памяти

Это происходит потому, что строки в Go неизменяемые (immutable). При “изменении” создаётся новая строка в новом месте памяти, а дескриптор обновляет указатель на неё.

Размеры типов в памяти

Разные типы занимают разное количество байт:

var intVar int = 100
var floatVar float64 = 3.14
var boolVar bool = true
var stringVar string = "Привет"

fmt.Printf("int: %d байт\n", unsafe.Sizeof(intVar))      // 8 байт
fmt.Printf("float64: %d байт\n", unsafe.Sizeof(floatVar)) // 8 байт
fmt.Printf("bool: %d байт\n", unsafe.Sizeof(boolVar))     // 1 байт
fmt.Printf("string: %d байт\n", unsafe.Sizeof(stringVar)) // 16 байт

Важно: string всегда занимает 16 байт независимо от длины текста, потому что хранит указатель + длину, а не сами символы.

Практика

Полный пример из урока

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // Переменная - это имя + тип + значение + адрес
    var age int = 35

    fmt.Println("=== Анатомия переменной ===")
    fmt.Printf("Имя: age\n")
    fmt.Printf("Тип: %T\n", age)
    fmt.Printf("Значение: %d\n", age)
    fmt.Printf("Адрес: %p\n", &age)
    fmt.Printf("Размер: %d байт\n", unsafe.Sizeof(age))

    // Изменение значения
    fmt.Println("\n=== После изменения ===")
    age = 40
    fmt.Printf("Новое значение: %d\n", age)
    fmt.Printf("Адрес (не изменился): %p\n", &age)

    // Разные переменные
    var temperature int = 25
    fmt.Println("\n=== Другая переменная ===")
    fmt.Printf("age: %d, адрес: %p\n", age, &age)
    fmt.Printf("temperature: %d, адрес: %p\n", temperature, &temperature)

    // Строки ведут себя иначе
    var stringVar string = "Привет, Go!"
    fmt.Printf("string: %d байт, адрес: %p\n", unsafe.Sizeof(stringVar), &stringVar)
    fmt.Printf("StringData адрес: %p\n", unsafe.StringData(stringVar))

    stringVar = "Привет, C#!"
    fmt.Printf("string: %d байт, адрес: %p\n", unsafe.Sizeof(stringVar), &stringVar)
    fmt.Printf("StringData адрес: %p\n", unsafe.StringData(stringVar))
}

Демонстрация изменения int vs string

Int — адрес не меняется:

var x int = 10
fmt.Printf("%p\n", &x)  // 0x1400001a0c8

x = 20
fmt.Printf("%p\n", &x)  // 0x1400001a0c8 (тот же!)

String — адрес данных меняется:

var s string = "Hello"
fmt.Printf("Переменная: %p\n", &s)                    // 0x1400000e018
fmt.Printf("Данные: %p\n", unsafe.StringData(s))      // 0x100f18e60

s = "World"
fmt.Printf("Переменная: %p\n", &s)                    // 0x1400000e018 (не изменился)
fmt.Printf("Данные: %p\n", unsafe.StringData(s))      // 0x100f18e80 (изменился!)

Важные моменты

1. Неизменяемость строк Строки в Go immutable — любое “изменение” создаёт новую строку. Старая строка остаётся в памяти до сборки мусора.

2. String — это дескриптор Переменная типа string хранит не сами символы, а метаданные о строке:

  • Указатель на массив байт
  • Длину строки в байтах

3. 16 байт для любой строки

unsafe.Sizeof("a")                    // 16 байт
unsafe.Sizeof("очень длинная строка") // 16 байт

Размер переменной не зависит от длины текста.

4. unsafe.StringData() Функция возвращает указатель на базовый массив байт строки. Это low-level операция для специфичных задач.

5. Два уровня адресации

  • &stringVar — адрес дескриптора (структуры)
  • unsafe.StringData(stringVar) — адрес реальных данных

6. Это работает не для всех типов одинаково Простые типы (int, float, bool) хранят значение напрямую. Строки, слайсы, мапы хранят дескрипторы, которые указывают на данные.

7. Подробнее о строках позже Это введение в особенности строк. Более детальный разбор внутреннего устройства будет в отдельном уроке о строках.

Что запомнить

  • Переменная = имя + тип + значение + адрес
  • При изменении значения адрес переменной не меняется
  • Разные переменные имеют разные адреса
  • Строка занимает 16 байт (дескриптор), независимо от длины
  • У строки два адреса: переменной и данных
  • При “изменении” строки адрес данных меняется (создаётся новая строка)
  • Адрес переменной-дескриптора остаётся прежним
  • Строки в Go неизменяемые (immutable)
  • & — адрес переменной
  • unsafe.StringData() — адрес данных строки
  • Разные типы по-разному располагаются в памяти

Полезные ссылки