Типы данных в Go

Практический туториал с примерами кода

Автор — Владимир Балун

ex-TeamLead в Яндекс
Содержание
Ты пришёл в Go после беззаботных экспериментов с динамическими языками. И первое, что бросается в глаза — строгая статическая типизация: каждую переменную надо снабжать меткой типа.

Сначала это кажется обузой: в Python ты мог написать a = 42, а потом a = «hello» и всё работало, пока внезапно не ломалось на продакшене. В Go такое не пройдёт — компилятор вмиг скажет «нет!» и укажет, что ты присвоил неправильный тип.

И поверь, как только ты отойдешь от крайностей, сразу начнёшь ценить эту систему. Ведь именно благодаря статической типизации Go ловит сотни ошибок ещё на этапе компиляции, предотвращая сумбур в коде и повышая его надежность и читаемость.

Типы данных в Go — это не помеха, а твой щит от глупых багов и неожиданных падений

Фундамент: простые типы как атомы мира Go

Всё начинается с простых атомов – базовых типов:
  • int и float64 для чисел
  • bool для флагов «истина/ложь»
  • string для текста

Каждый из них – как строительный блок, например:
  • bool – это переключатель «включено/ выключено»
  • int – целое число (32 или 64 бита, в зависимости от архитектуры)
  • float64 – число с плавающей точкой двойной точности (по умолчанию все дробные константы превращаются в float64)

Строка (string) в Go – особый случай: строка по сути – это неизменяемый массив байтов. Она может хранить произвольную последовательность байт, не только текст (например, в кодировке UTF-8).

Отдельно про rune

Чтобы корректно работать с символами, а не с сырыми байтами, в Go есть отдельный тип — rune. По сути, rune — это псевдоним для int32, представляющий кодовую точку Unicode.

То есть rune — это «символ как смысловая единица», независимо от того, сколько байтов он занимает.

Например:
var r rune = '😊'
fmt.Println(r) // 128522 – Unicode-код символа
Литералы вроде 'A', 'Я', '😊' - это rune. Они позволяют мыслить текстом, а не машинными байтами. Это особенно важно, когда работаешь с русским языком, китайским, арабским, эмодзи и комбинированными символами.

Полный список целочисленных типов в Go

тип
размер
int8
от -128 до 127 (1 байт)
int16
от -32768 до 32767 (2 байта)
int32
от -2147483648 до 2147483647 (4 байта)
int64
от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 (8 байт)
uint8
от 0 до 255 (1 байт)
uint16
от 0 до 65535 (2 байта)
uint32
от 0 до 4294967295 (4 байта)
uint64
от 0 до 18 446 744 073 709 551 615 (8 байт)
byte
псевдоним для uint8
rune
псевдоним для int32
int
4 байта на 32‑битных архитектурах и 8 байт на 64‑битных
int/uint
занимают столько же, сколько машинное слово: 4 байта на 32‑битных архитектурах и 8 байт на 64‑битных

Структуры (struct): собираем свой собственный конструктор

Когда одного кирпичика недостаточно, в Go есть структуры (struct) — объединение нескольких полей в одно представление пользовательского типа данных. Представь, что ты заполняешь анкету: в одном месте указываешь имя, в другом — возраст, и т. д. Точно так же struct собирает разные данные в единую сущность.

Например:
type User struct {
    Name string
    Age  int
}
Здесь мы создали тип User с двумя полями: Name и Age. Думай об этом как о конструкторе: ты определяешь шаблон (структуру), а потом наполняешь его данными. Это очень гибко: хочешь добавить адрес или электронную почту — просто допиши поле в struct.

Структуры — главный инструмент моделирования любых реальных объектов и записей в Go. Они помогают держать связанные данные вместе и делают код более наглядным: не куча разбросанных переменных, а единая сущность с понятными полями.

Ссылки и коллекции: указатели, срезы и мапы

В Go есть несколько типов, которые позволяют работать с адресами на данные и коллекциями.
Указатель (*T) — это просто переменная, хранящая адрес другой переменной. Представь указатель как координату или ссылку: зная адрес, можно попасть к нужным данным.

Например:
x := 5
p := &x     // p указывает на x
*p = 10     // меняем значение x через указатель
fmt.Println(x) // 10
Здесь p := &x взял адрес переменной x, а *p = 10 разыменовал этот адрес (как бы прыгнул на тот адрес), присвоив 10 прямо в x. Указатели дают контроль над тем, где именно лежат данные, но использовать их нужно осторожно и осмысленно.
Срез (slice) — более продвинутый способ работать с массивами. Если обычный массив имеет фиксированную длину, то срез — это концепция динамического массива, который может динамически расти. Его удобно назвать «швейцарским ножом» для работы с массивами: он хранит указатель на массив, текущую длину и емкость. Главное — срез можно расширять.
Словарь (map) — коллекция пар «ключ: значение». Представь обычный словарь или телефонный справочник: по ключу (например, имени) находишь соответствующее значение (телефон). Go-справочник устроен так же.

Пример:
phoneBook := map[string]string{
    "Alice": "+1-234-567",
}
fmt.Println(phoneBook["Alice"]) // "+1-234-567"
Благодаря тому, что под капотом стоит хеш-таблица, поиск по ключу очень быстрый.

Интерфейсы (interface)

Интерфейсы — это словно контракты или роли. Интерфейс задаёт набор методов (условий). Если твой тип реализует все эти методы, он невидимо «подписывается» на контракт и автоматически начинает вести себя как экземпляр этого интерфейса. Самый известный пример — встроенный интерфейс error. В Golang он определён очень просто:
type error interface {
    Error() string
}
То есть любое значение, у которого есть метод Error() string, соответствует интерфейсу error. Go просто говорит: «Если ты знаешь, как давать текст ошибки (через метод Error), ты можешь везде выступать в роли error». Ошибка — это обычное значение интерфейсного типа error, поэтому ты можешь создать свою собственную, в случае необходимости.

Пример пользовательской ошибки:
type MyError struct{ Text string }
func (e MyError) Error() string { return e.Text }

err := MyError{"что-то пошло не так"}
fmt.Println(err) // печатает "что-то пошло не так"
Здесь MyError автоматически стал error’ом благодаря методу Error().
Другой повсеместный пример — интерфейс fmt. Stringer, который требует метод String() string. Если твой тип реализует его, то при выводе (например, fmt. Println) он красиво форматируется.

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

Практический вывод

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

Статическая типизация заставляет думать о том, что хранится в каждой переменной, ещё до запуска программы. Это помогает избежать многих ловушек (неверные сравнения, неподходящие операции) и гарантирует производительность: компилятор уже знает типы данных и может хорошо оптимизировать код.

В итоге в реальных проектах это выливается в безопасность и простоту сопровождения. Код самодокументирован: по имени переменной и её типу данных сразу понятно, какую роль она играет. Меньше сюрпризов — больше уверенности. Поэтому не стоит сражаться с системой типов: прислушивайся к компилятору — он твой лучший друг. Компилятор подскажет, если что-то не так, и убережёт от большинства опечаток и недоразумений.
Но есть ещё один момент, о котором часто забывают, — выравнивание типов данных
В Go каждый примитивный тип данных должен начинаться в памяти по определённой границе. Это называется выравниванием (alignment)
Размер — количество байт, занимаемое типом данных
Выравнивание – оптимальное размещение в памяти для ускорения доступа
Примеры:
  • int8, byte — 1 байт
  • int16 — 2 байта
  • int32, float32 — 4 байта
  • int64, float64, указатели — размер машинного слова (8 байт на 64-битных системах и 4 байта на 32-битных)

Иными словами, значение int32 должно находиться по адресу, кратному 4, значение int16 — по адресу, кратному 2, и так далее.

Почему так работает?

Коротко: ради скорости.

Подробнее: современные CPU могут читать и писать данные размером в машинное слово атомарно, если адрес значения выровнен. Если же, например, на 32-битной системе значение int32 расположено «криво» — по адресу, кратному 2, но не кратному 4 — процессору приходится выполнять две операции чтения или записи вместо одной:
Однако за выравнивание приходится платить: размещая данные на ровных границах, Go иногда добавляет лишние байты паддинга, которые не используются. Это классический компромисс: мы жертвуем несколькими байтами памяти ради производительности.

Пример, когда выравнивание съедает лишнюю память:
type X struct {
    a int8
    b int64
}
Из-за порядка полей Go добавит 7 байт паддинга между a и b, поскольку b должен быть выровнен по 8 байтам (для 64-битных систем).

Итого:
  • логически: 1 + 8 = 9 байт
  • фактически: 16 байт

Когда важно понимать выравнивание?

  • При работе с большим количеством структур
    Например, у вас 10 млн объектов, и каждый из-за неудачного порядка полей занимает +8 лишних байт. Потеря — 80 МБ. В высоконагруженных сервисах это может быть критично.
  • При реализации сетевых протоколов и бинарных форматов
    Если требуется фиксированный размер сообщения, выравнивание может неожиданно добавить пустые байты, увеличивая трафик и время сериализации.
В конечном счёте типы данных в Go — это не просто объявления переменных, это образ мышления. Они заставляют тебя продумывать данные заранее, делая код не просто работающим, а предсказуемым и прочным. И в этом кроется главная элегантность Go — он помогает писать правильный и понятный код, а не усложняет жизнь.

Это и многое другое разбираем на практике — в нашем курсе по Golang для начинающих

Для учебы нужно знать основы Go — мы не будем изучать его с самого нуля. Наш курс подходит для разработчиков, которые уже знают базу или хотят перейти в Golang из другого языка программирования (C++, Java, Python и других) или стека: например, frontend, системного администрирования или mobile-разработки

Мы воссоздадим атмосферу работы внутри IT-компании и с нуля напишем таск-трекер на Golang. Обучение будет строиться не от теории к практике, а наоборот. Начнем писать код уже с первого занятия, а в теорию будем углубляться точечно. Получили рабочую задачу, прочитали ТЗ, посмотрели теорию — написали код.

Изучение теории, как на курсах с нуля, не дает практических навыков. База есть, а как что-то написать на работе — непонятно. И на собеседованиях показать нечего, а без опыта не берут.

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

Набор уже открыт

Другие статьи

    И чем раньше это понимаешь, тем быстрее начинаешь писать нормальный, предсказуемый код без странных багов. Здесь важен не только общий подход, но и понимание основы того, как язык позволяет хранить и изменять значения.

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

    Например, ты передал значение в функцию — и вроде бы все хорошо, но внутри ничего не поменялось. Почему? Потому что ты передал копию. А значит, работа шла уже с другими типами данных. В таких случаях важно определить, где нужен указатель, а где нет, ведь указатели напрямую влияют на обработки и итоговый результат.

    Дальше начинается самое интересное: ты либо осознанно управляешь этим процессом, либо ловишь неожиданный результат. Особенно это заметно, когда подключаются указатели — они позволяют работать с оригиналом, а не копией, но требуют понимания, что именно происходит в памяти. В противном случае легко получить ситуацию, где изменение в одном месте неожиданно влияет на другое, особенно при параллельного выполнении.

    Отдельный момент — обработка строк и форматирование с помощью fmt. Каждая операция может создавать новые значения, а значит увеличивать нагрузку. Даже такая мелочь, как кавычка — будь то двойная или другой формат — влияет на поведение строковые значений и на то, как они представлены в памяти.

    Также не забывай про структуры: они упрощают организацию типов данных, но при неправильном подходе усложняют код. Структуры могут содержать набор различных элементов, и важно, чтобы их объявления были продуманы. Чем проще и понятнее модель, тем легче поддержка и развитие проекта, особенно если в нем используются разные типы и сложные зависимости.

    В Go существует строгая система, где каждый оператор, каждая операция и даже выбор между базовыми и более сложными решениями влияет на итог. Например, использование map или struct — это уже вопрос архитектуры, а не просто синтаксиса.

    Когда ты работаешь с реальными проектами, особенно где есть серверы или сложная логика, важно учитывать, как происходит передача и изменение типов данных. Это напрямую влияет на производительность и масштабируемость.

    В итоге эти знания — это не просто язык и про его синтаксис. Это про контроль: как ты используешь типы данных и выстраиваешь логику программирования. Чем лучше ты это понимаешь типы данных, тем чище и эффективнее становится твой код.
    Работая с типами данных, ты постоянно управляешь их поведением