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) в соответствии со стандартными правилами арифметики дополнения до двух:
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. На битовом уровне число разделяется на три сегмента:
- Знак (Sign);
- Экспонента (Exponent);
- Мантисса (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-битных системах):
str— указатель (unsafe.Pointer) на начало байтового массива;len— точное количество байтов в строке (int).
// Так строка представлена в рантайме (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) обязано быть написано программистом в явном виде:
var i int32 = 42
// var f float64 = i // Ошибка компиляции!
var f float64 = float64(i) // Разрешено только явное преобразование
Это правило неукоснительно соблюдается даже для типов с полностью идентичным внутренним представлением. Если вы объявляете доменный тип поверх примитива:
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, порождая катастрофические уязвимости переполнения буфера.
Итог
boolзанимает 1 байт (в реализации gc) из-за аппаратных ограничений минимальной байтовой адресации шины памяти процессора.intиuintимеют переменную разрядность (4 или 8 байт), зависящую от целевой платформы (GOARCH). Для сетевых протоколов и бинарных форматов всегда используйте типы фиксированной ширины (int32,int64).- Mechanical Sympathy: Компактные типы данных (
int8,int16) в больших массивах позволяют в разы эффективнее использовать кэш-линии процессора L1/L2 (64 байта на x86-64). float: Строгая реализация стандарта IEEE-754. Никаких прямых сравнений через==и никаких расчетов денежных балансов.- Строки: Легковесная структура
stringStructиз двух полей (указательstrи длинаlen), обеспечивающая операциюlen(str)за $O(1)$. - Явная типизация: Никакого неявного сахара — все преобразования типов декларируются в коде абсолютно прозрачно.
Хотя string является базовым типом, работа с текстовыми данными в Go неразрывно связана с кодировкой UTF-8 и понятием рун (runes). В следующей лекции 7. Rune, Byte и Unicode в Go мы разберем, почему в языке нет типа char, как компилятор разделяет байты и символы, и почему наивная итерация по строке с кириллицей ломает текст на куски.