Анатомия переменной. Память, адреса и размеры
Краткое описание
В этом уроке углубляемся в понимание переменных: изучаем, как Go выделяет память под разные типы, как получить адрес переменной в памяти и узнать её размер. Используем пакет unsafe для анализа размера переменных и разбираемся, почему одни значения помещаются в определённые типы, а другие — нет.
Ключевые концепции
Переменная как область памяти
Переменная — это именованная область в оперативной памяти, которая хранит значение определённого типа. Когда объявляется переменная, происходит следующее:
- Компилятор выделяет нужное количество байт (в зависимости от типа)
- Операционная система через планировщик размещает эти байты где-то в памяти
- С этим участком памяти связывается имя переменной
- В память записывается значение
var age int = 35
// Выделяется 8 байт (на 64-битной системе)
// Связывается имя "age"
// Записывается значение 35
Четыре характеристики переменной
Каждая переменная имеет четыре важных свойства:
- Значение — что хранится
- Тип — какой тип данных
- Адрес — где в памяти находится
- Размер — сколько байт занимает
Получение информации о переменной
Значение переменной
Стандартный вывод через %d:
var age int = 35
fmt.Printf("%d\n", age) // 35
Тип переменной
Шаблон %T показывает тип:
fmt.Printf("%T\n", age) // int
Адрес в памяти
Оператор & возвращает адрес переменной, %p выводит его в шестнадцатеричном формате:
fmt.Printf("%p\n", &age) // 0x14000010108 (пример)
Важно: адрес меняется при каждом запуске программы. Планировщик операционной системы каждый раз выделяет программе разные участки памяти.
# Первый запуск
0x14000010108
# Второй запуск
0x1400001010c
# Третий запуск
0x14000010110
Размер переменной
Пакет unsafe.Sizeof() возвращает размер в байтах:
import "unsafe"
var age int = 35
fmt.Printf("%d байт\n", unsafe.Sizeof(age)) // 8 байт (на 64-битной системе)
Размер типов и архитектура
Архитектурно-зависимые типы
Тип int занимает разное количество байт в зависимости от архитектуры:
var x int = 100
// На 64-битной системе (macOS, современные Linux/Windows)
unsafe.Sizeof(x) // 8 байт (int64)
// На 32-битной системе
unsafe.Sizeof(x) // 4 байта (int32)
macOS, например, является исключительно 64-битной системой, поэтому int всегда равен int64.
Фиксированные типы
Типы с явным указанием размера всегда занимают одинаковое количество байт:
var a int8 = 127
unsafe.Sizeof(a) // 1 байт
var b int16 = 350
unsafe.Sizeof(b) // 2 байта
var c int32 = 35
unsafe.Sizeof(c) // 4 байта
var d int64 = 35
unsafe.Sizeof(d) // 8 байт
Переполнение и диапазоны
Почему 350 не влезает в int8
int8 занимает 1 байт = 8 бит. Максимальное значение для беззнакового 8-битного числа:
11111111 (в двоичной) = 255 (в десятичной)
Для знакового int8 диапазон: от -128 до 127.
Число 350 в двоичном виде:
101011110 — требуется 9 бит!
Память выделяется только байтами (не битами). 9 бит не влезают в 1 байт, поэтому нужен следующий размер — 2 байта (16 бит):
var x int8 = 350 // ОШИБКА! constant 350 overflows int8
var x int16 = 350 // OK, влезает в 16 бит
Минимальный тип для числа
Правило: выбирайте тип, в который гарантированно влезет ваше значение:
- 0-255 →
uint8(1 байт) - 0-65535 →
uint16(2 байта) - -128 до 127 →
int8(1 байт) - -32768 до 32767 →
int16(2 байта)
Практика
Полный пример анализа переменной
package main
import (
"fmt"
"unsafe"
)
func main() {
var age int16 = 350
// Значение
fmt.Printf("Значение: %d\n", age)
// Тип
fmt.Printf("Тип: %T\n", age)
// Адрес в памяти
fmt.Printf("Адрес: %p\n", &age)
// Размер
fmt.Printf("Размер: %d байт\n", unsafe.Sizeof(age))
}
Вывод:
Значение: 350
Тип: int16
Адрес: 0x14000010108
Размер: 2 байт
Эксперимент с разными типами
var a int8 = 100
var b int16 = 100
var c int32 = 100
var d int64 = 100
fmt.Printf("int8: %d байт\n", unsafe.Sizeof(a)) // 1 байт
fmt.Printf("int16: %d байт\n", unsafe.Sizeof(b)) // 2 байта
fmt.Printf("int32: %d байт\n", unsafe.Sizeof(c)) // 4 байта
fmt.Printf("int64: %d байт\n", unsafe.Sizeof(d)) // 8 байт
Даже если значение одинаковое (100), размер зависит от типа.
Адрес меняется при каждом запуске
var x int = 42
// Запуск 1
fmt.Printf("%p\n", &x) // 0x1400001a0c8
// Запуск 2
fmt.Printf("%p\n", &x) // 0x1400001a0d0
// Запуск 3
fmt.Printf("%p\n", &x) // 0x1400001a0b8
Это механизм безопасности операционной системы (ASLR — Address Space Layout Randomization).
Важные моменты
1. Пакет unsafe Название говорит само за себя — это “небезопасный” пакет для low-level операций. Используйте его только для обучения и отладки, не в production коде.
2. Память выделяется байтами Нельзя выделить 9 бит памяти. Минимальная единица — байт (8 бит). Если нужно 9 бит, выделяется 2 байта (16 бит).
3. Адрес записывается с &
Оператор & (амперсанд) возвращает адрес переменной в памяти. Это называется “взятие адреса” или “referencing”.
4. Шестнадцатеричная система
Адреса отображаются в 16-ричной системе с префиксом 0x. Например: 0x14000010108.
5. Размер зависит от типа, а не значения
var a int64 = 1
var b int64 = 1000000000
unsafe.Sizeof(a) == unsafe.Sizeof(b) // оба 8 байт
6. Компилятор не даст переполнить Go проверяет константы на этапе компиляции:
var x int8 = 350 // compile error: constant 350 overflows int8
Но runtime переполнение не проверяется:
var x int8 = 127
x = x + 1 // x станет -128 (wraparound)
Что запомнить
- Переменная = имя + тип + значение + адрес + размер в памяти
%d— значение,%T— тип,%pи&— адресunsafe.Sizeof()— размер переменной в байтах- Адрес меняется при каждом запуске программы
- Память выделяется байтами, не битами
- Тип определяет размер, не значение
int= 8 байт на 64-битных системах, 4 байта на 32-битных- Константы проверяются на переполнение при компиляции
- Число 350 требует минимум
int16(2 байта) - Пакет
unsafeтолько для обучения/отладки