· 10 мин 👁 1.5k Начинающий

Язык Go — инициализация: константы, переменные, init

Инициализация в Go — константы и iota, переменные с runtime-выражениями, функция init и порядок инициализации пакетов.

initконстантыiotaпеременныеинициализация
Содержание

Инициализация в Go выглядит похоже на C, но работает мощнее: сложные структуры собираются во время инициализации, а зависимости между пакетами разрешаются автоматически и в правильном порядке.

Константы

Константы в Go создаются на этапе компиляции — даже если объявлены внутри функции. Допустимые типы: числа, символы (руны), строки, булевы значения.

Выражение для константы должно быть вычислимо компилятором:

const A = 1 << 3        // ок — константное выражение, компилятор вычисляет сам
const B = len("hello")  // ок — len строкового литерала известен на этапе компиляции

// const C = math.Sin(math.Pi / 4) // ошибка компиляции:
// math.Sin вызывается в runtime, не в compile time

iota — перечисления без magic-чисел

iota — счётчик внутри блока const, начинается с 0 и увеличивается на 1 с каждой строкой. Удобен для перечислений и битовых масок:

type Weekday int

const (
    Sunday Weekday = iota // 0
    Monday                // 1
    Tuesday               // 2
    Wednesday             // 3
    Thursday              // 4
    Friday                // 5
    Saturday              // 6
)

fmt.Println(Monday, Friday) // 1 5

Выражения с iota повторяются неявно — каждая следующая строка использует то же выражение с новым значением iota:

type ByteSize float64

const (
    _           = iota                 // 0 — игнорируем первое значение
    KB ByteSize = 1 << (10 * iota)     // 1 << 10 = 1024
    MB                                 // 1 << 20
    GB                                 // 1 << 30
    TB                                 // 1 << 40
    PB                                 // 1 << 50
    EB                                 // 1 << 60
    ZB                                 // 1 << 70
    YB                                 // 1 << 80
)

fmt.Println(KB, MB, GB) // 1024 1.048576e+06 1.073741824e+09

Первое значение игнорируется через _ — так KB получает iota == 1, а не 0.

Метод String() для ByteSize

Реализация String() string на пользовательском типе позволяет управлять его выводом через fmt. ByteSize с методом String() сама знает, как себя напечатать:

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

fmt.Println(YB)              // 1.00YB
fmt.Println(ByteSize(1e13))  // 9.09TB
fmt.Println(ByteSize(512))   // 512.00B

Здесь нет рекурсии, хотя String() вызывает Sprintf — потому что используется глагол %f, а не %s или %v. Sprintf вызывает String() только когда хочет строку, а %f ожидает число.

Переменные

Переменные инициализируются как константы, но выражение может быть вычислено в runtime — вызов функции, обращение к окружению, любая логика:

var (
    home   = os.Getenv("HOME")   // читаем переменную окружения в runtime
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

Такие переменные на уровне пакета инициализируются до вызова main — в порядке объявления, с учётом зависимостей.

Ещё пример — инициализация через вызов функции:

var (
    httpClient = &http.Client{Timeout: 10 * time.Second} // создаём клиент один раз
    startTime  = time.Now()                               // фиксируем время старта
)

Функция init

Каждый файл может объявить одну или несколько функций init() — без аргументов и без возвращаемых значений. Go вызывает их автоматически:

все импортированные пакеты инициализированы
    → переменные пакета вычислены
        → вызываются функции init()
            → main()
func init() {
    if user == "" {
        log.Fatal("$USER not set") // прерываем запуск, если окружение неполное
    }
    if home == "" {
        home = "/home/" + user // задаём разумное значение по умолчанию
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath можно переопределить флагом командной строки
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

init не вызывается явно и не может быть вызвана из кода — только рантаймом.

Несколько init в одном файле и пакете

В одном файле может быть несколько init — они выполняются в порядке объявления. В пакете из нескольких файлов Go обрабатывает файлы в алфавитном порядке:

// file: setup.go
func init() {
    fmt.Println("init 1") // выполнится первой
}

func init() {
    fmt.Println("init 2") // выполнится второй
}

Типичные применения init

Регистрация драйверов и плагинов — стандартный паттерн в Go. Импорт пакета ради его init без использования экспортируемых имён:

import (
    "database/sql"
    _ "github.com/lib/pq" // blank import: пакет не используется напрямую,
                           // но его init() регистрирует postgres-драйвер в database/sql
)

func main() {
    db, err := sql.Open("postgres", "...") // драйвер уже зарегистрирован
    _ = db
    _ = err
}

Предвычисление таблиц значений:

var sineTable [360]float64

func init() {
    for i := range sineTable {
        sineTable[i] = math.Sin(float64(i) * math.Pi / 180)
        // math.Sin нельзя использовать в константе — вычисляем в init
    }
}

Порядок инициализации между пакетами

Go гарантирует: если пакет A импортирует пакет B, то B полностью инициализируется до A. Циклические импорты запрещены — компилятор не пропустит:

пакет main импортирует пакет db
пакет db импортирует пакет config

порядок: config → db → main

Итоги

  • Константы вычисляются на этапе компиляции — только числа, руны, строки, булевы значения
  • iota — счётчик в блоке const, удобен для перечислений и битовых масок
  • Выражения в блоке const повторяются неявно — каждая строка использует то же выражение с новым iota
  • Переменные пакета могут инициализироваться runtime-выражениями: вызовы функций, os.Getenv и т.д.
  • init() вызывается автоматически после инициализации переменных пакета
  • В одном файле может быть несколько init — выполняются по порядку
  • Импортированные пакеты всегда инициализируются раньше импортирующих
  • _ "pkg" — blank import для запуска init() без использования пакета напрямую