Введение
В этой главе мы разбираемся с тем, из чего сделаны данные в Go: переменными, типами и константами. Тема кажется базовой, но именно здесь Go принимает несколько решений, которые отличают его от динамических языков и даже от многих статических. Здесь нет неявных преобразований типов, здесь у каждой переменной есть гарантированное начальное значение, а константы вычисляются на этапе компиляции и умеют работать с числами произвольной точности.
Мы научимся объявлять переменные двумя способами и поймём, когда какой уместен; разберём нулевые значения и почему благодаря им в Go почти нет «неинициализированной памяти»; пройдёмся по примитивным типам; познакомимся с пустым идентификатором; и подробно изучим константы вместе с идиоматичным механизмом iota. В конце заглянем в современные числовые литералы — разделители разрядов, двоичные и шестнадцатеричные записи.
Два способа объявить переменную
В Go есть полная форма через var и короткая через :=. Полная форма явно называет тип и работает как на уровне пакета, так и внутри функций:
var age int = 33
var city string = "Москва"
var ready bool = true
Если задаёшь начальное значение, тип можно опустить — компилятор выведет его сам:
var age = 33 // int
var city = "Москва" // string
Короткая форма := — это одновременно объявление и присваивание с выводом типа. Она лаконичнее и потому применяется чаще всего, НО работает только ВНУТРИ функций:
func main() {
age := 33
city := "Москва"
ready := true
_ = ready // чтобы компилятор не ругался на неиспользуемую переменную
}
Почему два способа? Идиома такая: := внутри функций для локальных переменных (это 90% случаев), а var — когда нужно объявить переменную БЕЗ начального значения (положиться на нулевое), на уровне пакета, или когда явно хочется зафиксировать тип, отличный от выводимого по умолчанию. Например, литерал 3.5 по умолчанию float64, и если нужен float32, это придётся указать явно.
Нулевые значения
В Go нет понятия «неинициализированная переменная». Если ты объявил переменную через var без значения, она получает НУЛЕВОЕ значение своего типа. Это осознанное проектное решение: оно убирает целый класс багов, связанных с чтением мусора из памяти.
package main
import "fmt"
func main() {
var a int // 0
var b float64 // 0
var c string // "" (пустая строка)
var d bool // false
fmt.Printf("%v %v %q %v
", a, b, c, d)
}
Запомни нулевые значения примитивов: у всех числовых типов это 0, у строк — пустая строка "", у булевого — false. У ссылочных типов (указатели, срезы, карты, каналы, функции, интерфейсы) нулевое значение — nil. Практический вывод: структуру можно использовать сразу после объявления, не заполняя каждое поле, и она будет в предсказуемом состоянии. Это очень по-гошному — минимум церемоний.
Примитивные типы
Набор встроенных типов в Go небольшой и понятный. Целые числа бывают со знаком (int8, int16, int32, int64) и без знака (uint8 … uint64). Есть «машинный» int, который на 64-битной платформе равен 64 битам — его и используют по умолчанию для счётчиков и индексов, не задумываясь о разрядности. Числа с плавающей точкой — float32 и float64, по умолчанию берут float64. Логический тип — bool. Строки — string.
var i int = 42
var big int64 = 9_000_000_000
var pi float64 = 3.14159
var flag bool = true
var name string = "Go"
Отдельно стоит запомнить два псевдонима, которые часто встречаются. byte — это синоним uint8, используется, когда речь про сырые байты (содержимое файла, тело HTTP-запроса). rune — синоним int32, хранит одну код-точку Юникода, о чём мы говорили в первой главе. Это не отдельные типы, а именно псевдонимы, но они делают код самодокументируемым: увидев []byte, читатель понимает «это байты», а []rune — «это символы».
Никаких неявных преобразований
Go принципиально не приводит числовые типы автоматически. Сложить int и float64 напрямую нельзя — это ошибка компиляции. Такое поведение раздражает первые дни, но защищает от коварных багов точности, которые в C или JavaScript прячутся до продакшена.
var count int = 3
var price float64 = 19.99
// total := count * price // ОШИБКА: mismatched types
total := float64(count) * price // так правильно — явное преобразование
fmt.Println(total) // 59.97
Конструкция float64(count) — это преобразование типа: ты явно говоришь «трактуй это целое как число с плавающей точкой». Каждый раз, когда типы не совпадают, ты обязан преобразовать их руками. Это делает места потенциальной потери точности видимыми прямо в коде.
Пустой идентификатор
Пустой идентификатор _ (подчёркивание) — это переменная «в никуда». Всё, что ты в неё присваиваешь, немедленно отбрасывается. Зачем такое нужно? Потому что Go заставляет использовать каждую объявленную переменную, а функции в Go часто возвращают несколько значений, из которых тебе интересны не все.
package main
import (
"fmt"
"strconv"
)
func main() {
// strconv.Atoi возвращает (int, error). Ошибку здесь сознательно игнорируем.
n, _ := strconv.Atoi("42")
fmt.Println(n) // 42
// Обратный случай — берём только ошибку, значение не нужно.
_, err := strconv.Atoi("не число")
fmt.Println(err) // strconv.Atoi: parsing "не число": invalid syntax
}
Здесь _ позволяет проигнорировать ненужную часть множественного возврата, не заводя переменную-пустышку, которую пришлось бы потом «использовать». В цикле range подчёркивание тоже незаменимо, когда нужен только индекс или только значение — увидим это в главе 3. Ещё одно применение — импорт ради побочного эффекта: import _ "github.com/lib/pq" подключает пакет, чтобы сработала его функция инициализации, но напрямую его имя нам не нужно.
Константы
Константа — это значение, зафиксированное на этапе компиляции. Объявляется словом const. В отличие от переменных, константы нельзя изменить в рантайме, и им нельзя присвоить значение, которое вычисляется во время выполнения (например, результат вызова функции). Зато у них есть суперспособность: нетипизированные константы работают с числами произвольной точности и подстраиваются под контекст.
const pi = 3.14159
const greeting = "Привет"
const maxUsers = 10_000
const (
statusActive = "active"
statusInactive = "inactive"
statusBanned = "banned"
)
Блок const (...) удобен для группировки связанных констант. Важная тонкость: константа без явного типа (const pi = 3.14159) называется НЕтипизированной. Она подстраивается под место использования — её можно присвоить и в float64, и в float32 без явного преобразования. Это делает константы гибче переменных: const big = 1 << 62 вычислится компилятором, даже если такое число не влезло бы в промежуточный int где-то по пути.
iota: генератор последовательностей
Когда нужен набор связанных целочисленных констант (уровни логирования, состояния конечного автомата, дни недели), в Go есть iota — счётчик, который автоматически увеличивается на 1 в каждой строке блока const. Начинается он с нуля и сбрасывается в начале каждого нового блока const.
const (
a = iota // 0
b = iota // 1
c = iota // 2
)
Повторять = iota в каждой строке необязательно: если правую часть опустить, Go повторит предыдущее выражение, а iota всё равно продолжит расти. Поэтому идиоматичная запись такая:
type Weekday int
const (
Monday Weekday = iota // 0
Tuesday // 1
Wednesday // 2
Thursday // 3
Friday // 4
Saturday // 5
Sunday // 6
)
Здесь мы завели свой тип Weekday на основе int и присвоили дням значения 0–6 одним элегантным блоком. Если позже вставить новый день в середину, все последующие значения пересчитаются автоматически — не нужно вручную править числа. Это защищает от ошибок при рефакторинге.
С iota можно строить и более хитрые вещи — например, битовые флаги через сдвиг:
type Permission uint
const (
Read Permission = 1 << iota // 1 (0b001)
Write // 2 (0b010)
Execute // 4 (0b100)
)
// Комбинируем флаги побитовым ИЛИ:
perm := Read | Write
fmt.Println(perm) // 3
// Проверяем наличие флага побитовым И:
canWrite := perm&Write != 0
fmt.Println(canWrite) // true
Выражение 1 << iota сдвигает единицу влево на номер строки, давая степени двойки: 1, 2, 4. Такие флаги можно объединять оператором | и проверять оператором &. Это классический приём для компактного хранения набора булевых опций в одном числе — ровно так устроены права доступа в файловых системах.
Современные числовые литералы
Начиная с Go 1.13, в числовых литералах можно ставить подчёркивание как разделитель разрядов. На значение оно не влияет — компилятор его игнорирует, — зато крупные числа читаются в разы легче.
const million = 1_000_000
salary := 75_000
const lightSpeed = 299_792_458 // м/с
fmt.Println(1000000 == 1_000_000) // true — значение то же самое
Разделитель работает во всех системах счисления. Двоичные литералы пишутся с префиксом 0b, восьмеричные — 0o, шестнадцатеричные — 0x. Группировать разряды подчёркиванием особенно удобно именно здесь: двоичное по полубайтам, hex-цвет по байтам.
mask := 0b1010_1100 // двоичное = 172
color := 0xFF_88_00 // hex RGB
perms := 0o755 // восьмеричные права файла
fmt.Printf("%d %d %d
", mask, color, perms)
Подчёркивание нельзя ставить в начале или конце числа, а также вплотную к префиксу или десятичной точке — только МЕЖДУ цифрами. Эти литералы делают числовой код самодокументируемым: 0xFF_88_00 сразу читается как три байта цвета, а 1_000_000 — как миллион, без мысленного пересчёта нулей.
Кейс из реального проекта
Соберём кусочек конфигурации HTTP-сервиса: набор констант для уровней логирования через iota, лимиты через литералы с разделителями и структуру настроек с нулевыми значениями по умолчанию. Это ровно то, что встречается в начале почти любого Go-сервиса.
package config
import "time"
// LogLevel — уровень детализации логов. iota задаёт порядок от тихого к шумному.
type LogLevel int
const (
LevelError LogLevel = iota // 0 — только ошибки
LevelWarn // 1
LevelInfo // 2
LevelDebug // 3 — всё подряд
)
// Лимиты сервиса. Разделители разрядов делают числа читаемыми с первого взгляда.
const (
MaxUploadBytes = 10_000_000 // 10 МБ
MaxConnections = 1_024
RequestTimeout = 30 * time.Second
)
// Config — настройки сервера. Нулевые значения полей — разумные умолчания.
type Config struct {
Port int // 0 по умолчанию, зададим явно при загрузке
Level LogLevel // LevelError по умолчанию — самый безопасный
Verbose bool // false
Hostname string // "" — заполним из окружения
}
Обрати внимание, как работают проектные решения языка вместе. iota задаёт уровни логирования без ручной нумерации — вставишь новый уровень, и порядок пересчитается сам. Литералы 10_000_000 и 1_024 читаются мгновенно. А структура Config благодаря нулевым значениям валидна сразу после создания через config.Config{}: порт будет 0, уровень — самый строгий LevelError, флаги выключены. Дальше код загрузки просто переопределит нужные поля, а всё остальное останется в безопасном состоянии по умолчанию.
Типичные ошибки
Попытка использовать := на уровне пакета
Короткое объявление работает только внутри функций. На уровне пакета оно не компилируется — там нужен var или const.
package main
count := 0 // ОШИБКА: non-declaration statement outside function body
var count = 0 // так правильно
Смешивание типов в арифметике
Забыл про запрет неявных преобразований — получил ошибку компиляции. Особенно часто ловят при делении целого на число с плавающей точкой.
total := 100
count := 3
// avg := total / count // это ЦЕЛОЧИСЛЕННОЕ деление: 33, дробь потеряна
avg := float64(total) / float64(count) // 33.333...
fmt.Println(avg)
Тут двойная грабля: мало того что типы должны совпадать, так ещё и деление двух int отбрасывает дробную часть. Приводи к float64 ДО деления, а не после.
Ожидание, что iota считает вызовы, а не строки
iota увеличивается на каждой СТРОКЕ блока const, а не на каждой константе. Если объявить несколько констант в одной строке, у них будет одинаковое значение iota. И iota сбрасывается в 0 в каждом новом блоке const — про это часто забывают.
const (
x, y = iota, iota // обе = 0 (одна строка)
z = iota // 1
)
Пустой идентификатор ради «заглушить компилятор»
Присваивать реальные ошибки в _, чтобы не разбираться с ними — плохая привычка. n, _ := strconv.Atoi(s) в продакшене прячет проблему: если строка не число, n будет 0, и ты получишь молчаливый баг. Игнорируй ошибку через _ только когда ты действительно уверен, что она невозможна или неважна.
Практика
- Объяви четыре переменные разных примитивных типов через
varбез начального значения и распечатай их нулевые значения через%vи%q. - Напиши программу, которая делит два целых числа и выводит результат с дробной частью. Убедись, что без преобразования к
float64дробь теряется. - Заведи тип
Direction intи черезiotaзадай константыNorth,East,South,West. Вставь новое направление в середину и проверь, что значения пересчитались. - Реализуй набор битовых флагов прав доступа (
Read,Write,Execute) через1 << iota, скомбинируй два флага оператором|и проверь наличие третьего оператором&. - Запиши число
1073741824как двоичный и как шестнадцатеричный литерал с разделителями разрядов и убедись через сравнение, что значения равны исходному.
Итог
Теперь ты умеешь объявлять переменные через var и := и знаешь, когда какой способ уместен; понимаешь нулевые значения и то, как они убирают целый класс багов; ориентируешься в примитивных типах и помнишь про запрет неявных преобразований; используешь пустой идентификатор осознанно; и владеешь константами вместе с iota и современными числовыми литералами. Это строительные блоки любой программы. В следующей главе мы соберём из них логику: разберём единственный цикл Go for во всех его формах, перебор через range, новинку for i := range 10 из Go 1.22, ветвления if/else и мощный switch с fallthrough и переключением по типу.