Изменяемость переменных и особенности строк в памяти
Краткое описание
В этом уроке углубляемся в работу переменных в памяти: изучаем, что происходит при изменении значения, почему адрес переменной остаётся прежним, и разбираем особое поведение строк. Узнаём, чем строки отличаются от других типов при хранении в памяти.
Ключевые концепции
Четыре составляющие переменной
Каждая переменная характеризуется четырьмя свойствами:
- Имя — идентификатор для обращения в коде
- Тип — какой тип данных хранится
- Значение — текущие данные
- Адрес — место в оперативной памяти
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()— адрес данных строки- Разные типы по-разному располагаются в памяти