Глава 2. Переменные, типы и константы

462 просмотров
0 лайков
0 в избранном

Введение

В этой главе мы разбираемся с тем, из чего сделаны данные в 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) и без знака (uint8uint64). Есть «машинный» 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, и ты получишь молчаливый баг. Игнорируй ошибку через _ только когда ты действительно уверен, что она невозможна или неважна.

Практика

  1. Объяви четыре переменные разных примитивных типов через var без начального значения и распечатай их нулевые значения через %v и %q.
  2. Напиши программу, которая делит два целых числа и выводит результат с дробной частью. Убедись, что без преобразования к float64 дробь теряется.
  3. Заведи тип Direction int и через iota задай константы North, East, South, West. Вставь новое направление в середину и проверь, что значения пересчитались.
  4. Реализуй набор битовых флагов прав доступа (Read, Write, Execute) через 1 << iota, скомбинируй два флага оператором | и проверь наличие третьего оператором &.
  5. Запиши число 1073741824 как двоичный и как шестнадцатеричный литерал с разделителями разрядов и убедись через сравнение, что значения равны исходному.

Итог

Теперь ты умеешь объявлять переменные через var и := и знаешь, когда какой способ уместен; понимаешь нулевые значения и то, как они убирают целый класс багов; ориентируешься в примитивных типах и помнишь про запрет неявных преобразований; используешь пустой идентификатор осознанно; и владеешь константами вместе с iota и современными числовыми литералами. Это строительные блоки любой программы. В следующей главе мы соберём из них логику: разберём единственный цикл Go for во всех его формах, перебор через range, новинку for i := range 10 из Go 1.22, ветвления if/else и мощный switch с fallthrough и переключением по типу.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!