6. Базовые типы данных. int, float, bool, string

В языке Go тип данных — это не абстрактная концепция из академического учебника. Это строжайший физический контракт с компилятором, который точно определяет, сколько байтов оперативной памяти необходимо выделить на стеке или в куче, и по каким правилам процессор обязан манипулировать этими битами в регистрах (как целым числом со знаком, как мантиссой с плавающей точкой или как парой из указателя и длины).

Как бэкенд-инженеры, мы обязаны воспринимать базовые типы не просто как «числа и строки», а как конкретные блоки памяти, напрямую влияющие на загрузку кэш-линий процессора и расход оперативной памяти высоконагруженного сервера.


Логический тип: bool

Тип bool может принимать одно из двух значений: true либо false. Нулевым значением по умолчанию (Zero Value) для него является false.


Целочисленные типы: int, uint и их братья

Go предоставляет две группы целочисленных типов: платформозависимые (int, uint, uintptr) и типы с жестко фиксированной разрядностью (int8, int16, int32, int64 и их беззнаковые собратья uint8, uint16, uint32, uint64).

Платформозависимый int

Когда вы объявляете переменную через var x int или используете краткую запись x := 10, компилятор выбирает системный тип int. Его физическая разрядность целиком диктуется целевой архитектурой компиляции (GOARCH):

flowchart TD
    Source["Декларация: var x int"] --> Compiler{"Целевая архитектура GOARCH"}
    Compiler -->|"amd64, arm64"| A64["Размер: 8 байт (64 бита)<br>Диапазон: от -2^63 до 2^63-1"]
    Compiler -->|"386, arm"| A32["Размер: 4 байта (32 бита)<br>Диапазон: от -2^31 до 2^31-1"]

    A64 -.-> TypeMismatch["Внимание: int и int64 несовместимы!"]
    A32 -.-> TypeMismatch
    TypeMismatch --> Explicit["Обязателен явный каст: int64(x)"]

    style Source fill:#1e293b,stroke:#818cf8,stroke-width:2px,color:#e2e8f0
    style A64 fill:#132e1b,stroke:#22c55e,stroke-width:2px,color:#bbf7d0
    style A32 fill:#1e1e38,stroke:#38bdf8,stroke-width:2px,color:#e2e8f0
    style TypeMismatch fill:#2d1515,stroke:#ef4444,stroke-width:2px,color:#fecaca
  

Mechanical Sympathy: Размер имеет значение

В повседневной разработке принято использовать системный int для индексов циклов или локальных счетчиков. Но когда сервис оперирует массивами и срезами (slice) из сотен тысяч и миллионов элементов, выбор разрядности становится критическим фактором производительности.

Физическая кэш-линия процессоров x86-64 (кэш L1/L2) имеет размер 64 байта (на серверах ARM64 и Apple M-серии рантайм Go закладывает 128 байт):

  • Если вы используете срез []int64, в одну кэш-линию поместится ровно 8 элементов ($64 / 8$);
  • Если диапазон чисел позволяет обойтись типом []int8, в одну кэш-линию войдет сразу 64 элемента!

Итерирование по срезу []int8 генерирует в восемь раз меньше промахов кэша (Cache Misses) и холостых обращений к медленной шине оперативной памяти RAM. Аппаратный предвыборщик процессора (Hardware Prefetcher) загружает данные непрерывным потоком, обеспечивая максимальный темп вычислений. Однако при проектировании структур из разнородных полей важно помнить о правилах выравнивания памяти (Memory Padding), о которых мы детально поговорим в статье о структурах.

Переполнение (Integer Overflow)

В Go целочисленное переполнение намеренно не вызывает паники (panic) в рантайме. Процессор циклически оборачивает значение (wrap-around) в соответствии со стандартными правилами арифметики дополнения до двух:

Go
var max int8 = 127
max = max + 1
fmt.Println(max) // Выведет -128

Если бизнес-логика требует гарантированного обнаружения переполнения (например, в финансовых расчетах, учете балансов или биллинге), применяйте специализированные функции из стандартного пакета math/bits (например, bits.Add64()), которые возвращают флаг переноса (carry out). Учтите: пакет math/bits работает только с беззнаковыми числами; переполнение знаковых проверяйте сравнением (например, a > 0 && b > math.MaxInt64-a).


Числа с плавающей точкой: float32, float64

В Go полностью отсутствуют размытые типы float или double. Вы обязаны декларировать точность явно: float32 (одинарная точность, 4 байта) либо float64 (двойная точность, 8 байт). Краткая форма := для дробных чисел всегда выбирает float64.

Под капотом операции с плавающей точкой строго следуют международному стандарту IEEE-754. На битовом уровне число разделяется на три сегмента:

  1. Знак (Sign);
  2. Экспонента (Exponent);
  3. Мантисса (Fraction / Mantissa).

Типы float также поддерживают специальные математические состояния: NaN (Not a Number), +Inf (положительная бесконечность) и -Inf (отрицательная бесконечность). Проверять их наличие следует с помощью функций стандартного пакета math: например, math.IsNaN(f).

Для полноты картины: в Go есть и встроенные комплексные числа — complex64 (две float32) и complex128 (две float64). Их создают функцией complex(re, im) или литералом 1 + 2i, а части извлекают встроенными real(z) и imag(z).


Строки: string

Строка в Go (string) — это неизменяемая (immutable) непрерывная последовательность байтов (в 99% случаев представляющая собой валидный UTF-8 текст).

В недрах рантайма строка — это компактная двухсловная структура stringStruct (занимающая ровно 16 байт на 64-битных системах):

  1. str — указатель (unsafe.Pointer) на начало байтового массива;
  2. len — точное количество байтов в строке (int).
Go
// Так строка представлена в рантайме (runtime/string.go)
type stringStruct struct {
    str unsafe.Pointer
    len int
}

(Похожая структура reflect.StringHeader с полями Data uintptr и Len int с Go 1.21 объявлена устаревшей: её поле Data — просто число, оно не удерживает данные от сборщика мусора.)

Благодаря такой архитектуре:

  • Вычисление длины строки len(str) — это мгновенная операция сложности $O(1)$, считывающая значение счетчика len;
  • Передача строки в функцию или операция копирования a = b копирует лишь 16 байт структуры заголовка, не затрагивая сами текстовые данные в куче или сегменте констант.

Поскольку строки скрывают множество тонкостей при работе с кириллическими символами и эмодзи, мы посвятим их детальному препарированию отдельную лекцию: 20. Строки в Go. Immutable string и работа с Unicode.


Строгость приведения типов

В Go действует принцип бескомпромиссной явности: в языке полностью отсутствует неявное приведение типов (Implicit Casting), привычное разработчикам на C, C++ или C#. Любое преобразование (Type Conversion) обязано быть написано программистом в явном виде:

Go
var i int32 = 42
// var f float64 = i        // Ошибка компиляции!
var f float64 = float64(i) // Разрешено только явное преобразование

Это правило неукоснительно соблюдается даже для типов с полностью идентичным внутренним представлением. Если вы объявляете доменный тип поверх примитива:

Go
type UserID int64
var id UserID = 100

var x int64 = 50
// x = id        // Ошибка компиляции: Cannot use 'id' (type UserID) as type int64
x = int64(id)   // Только явный каст!

Такая строгая дисциплина полностью ликвидирует скрытые ошибки, когда компилятор C++ незаметно от разработчика приводил отрицательное число к unsigned int, порождая катастрофические уязвимости переполнения буфера.


Итог

  1. bool занимает 1 байт (в реализации gc) из-за аппаратных ограничений минимальной байтовой адресации шины памяти процессора.
  2. int и uint имеют переменную разрядность (4 или 8 байт), зависящую от целевой платформы (GOARCH). Для сетевых протоколов и бинарных форматов всегда используйте типы фиксированной ширины (int32, int64).
  3. Mechanical Sympathy: Компактные типы данных (int8, int16) в больших массивах позволяют в разы эффективнее использовать кэш-линии процессора L1/L2 (64 байта на x86-64).
  4. float: Строгая реализация стандарта IEEE-754. Никаких прямых сравнений через == и никаких расчетов денежных балансов.
  5. Строки: Легковесная структура stringStruct из двух полей (указатель str и длина len), обеспечивающая операцию len(str) за $O(1)$.
  6. Явная типизация: Никакого неявного сахара — все преобразования типов декларируются в коде абсолютно прозрачно.

Хотя string является базовым типом, работа с текстовыми данными в Go неразрывно связана с кодировкой UTF-8 и понятием рун (runes). В следующей лекции 7. Rune, Byte и Unicode в Go мы разберем, почему в языке нет типа char, как компилятор разделяет байты и символы, и почему наивная итерация по строке с кириллицей ломает текст на куски.