Язык Go — инициализация: константы, переменные, init
Инициализация в Go — константы и iota, переменные с runtime-выражениями, функция init и порядок инициализации пакетов.
Содержание
Инициализация в 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()без использования пакета напрямую