Анатомия переменной. Память, адреса и размеры

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

В этом уроке углубляемся в понимание переменных: изучаем, как Go выделяет память под разные типы, как получить адрес переменной в памяти и узнать её размер. Используем пакет unsafe для анализа размера переменных и разбираемся, почему одни значения помещаются в определённые типы, а другие — нет.

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

Переменная как область памяти

Переменная — это именованная область в оперативной памяти, которая хранит значение определённого типа. Когда объявляется переменная, происходит следующее:

  1. Компилятор выделяет нужное количество байт (в зависимости от типа)
  2. Операционная система через планировщик размещает эти байты где-то в памяти
  3. С этим участком памяти связывается имя переменной
  4. В память записывается значение
var age int = 35
// Выделяется 8 байт (на 64-битной системе)
// Связывается имя "age"
// Записывается значение 35

Четыре характеристики переменной

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

  1. Значение — что хранится
  2. Тип — какой тип данных
  3. Адрес — где в памяти находится
  4. Размер — сколько байт занимает

Получение информации о переменной

Значение переменной

Стандартный вывод через %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 только для обучения/отладки

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