1. Первое знакомство с const
До этого момента мы работали в основном с переменными — значениями, которые можно менять по ходу выполнения программы:
var age int = 18
Переменная живёт, изменяется, зависит от ввода пользователя, от логики программы, от условий. Сегодня появляется новое слово — const. Оно обозначает константу: значение, которое задаётся один раз и больше никогда не меняется.
Например:
const maxUsers = 100
После этого maxUsers уже нельзя переприсвоить. Если вы попробуете написать maxUsers = 200, программа просто не скомпилируется. И это не «жёсткость ради жёсткости», а осознанная защита: если по смыслу значение не должно изменяться, лучше зафиксировать это на уровне языка.
Важно понимать ещё одну вещь: константа должна быть известна во время компиляции. То есть её значение нельзя получить из fmt.Scan, из файла или из сети. Например, так написать нельзя:
var x int
fmt.Scan(&x)
const y = x // ошибка
Компилятор не знает, чему будет равен x во время выполнения, а значит не может сделать из него константу. Константы — это про правила и фиксированные величины, а не про текущее состояние программы.
Очень часто константы используются для «жёстких границ» и настроек, которые по смыслу не должны меняться:
const minAge = 18
if age >= minAge {
fmt.Println("welcome")
}
Здесь minAge — это правило системы. Если бы это была обычная переменная, её можно было бы случайно изменить, и логика программы стала бы менее предсказуемой. Константа же делает намерение явным: это фиксированное значение.
Группа констант
В Go константы часто объявляют не по одной, а группой — через const ( ... ). Это просто удобный способ держать «правила» рядом: границы, лимиты, строки сообщений, коды режимов. Смысл при этом не меняется: каждая строка внутри блока — обычная константа, просто записанная компактнее.
const (
minAge = 18
maxUsers = 100
appName = "DoorGuard"
)
Такой блок читается как маленькая «табличка настроек»: видно, что это фиксированные значения, и они живут вместе. А дальше всё остаётся тем же — константы нельзя менять, и они должны быть вычислимы на этапе компиляции.
Возможно вам кажется, что const — это просто «var, только без возможности изменить значение». Но дальше выяснится, что в Go константы устроены интереснее. У них есть особенность: они могут быть typed и untyped, и именно это объясняет, почему иногда константы ведут себя гибче, чем переменные.
2. Untyped константы и тип по умолчанию
Если вы только привыкли к мысли «тип — это закон», Go внезапно делает финт ушами: int нельзя складывать с int64, но можно написать time.Second / 1e3 или math.Sqrt(2) — и компилятор не требует от вас плясок с int64(2) и float64(2). Это не магия и не «поблажка новичкам», а осознанный дизайн: константы в Go живут чуть в другом мире, чем переменные.
Чтобы в этом мире ориентироваться, нам нужно выучить два термина:
- untyped константа — константа без закреплённого Go-типа;
- typed константа — константа с явно указанным типом (например, const x int = 5).
Из-за этой разницы константы «ведут себя иначе», чем переменные: иногда «подстраиваются под контекст», а иногда — упрямо требуют явной конверсии.
Untyped: значение есть, типа нет
Когда вы пишете:
const n = 5
интуитивно хочется сказать: «ну это же int». Но корректнее думать так: это число 5, которое пока не обязано быть int/int64/uint. Оно «без паспорта» и может получить паспорт позже — в зависимости от того, куда вы его используете.
То же самое легко увидеть на строках. Строковый литерал "Hello, 世界" — это не «значение типа string» в том же смысле, что переменная var s string. Это untyped string constant, и он может быть присвоен туда, где ожидается строковый тип, не создавая конфликтов типов.
Мини-пример: одна константа подходит к разным типам
package main
import "fmt"
func main() {
const n = 5 // untyped
var a int = n
var b int64 = n
fmt.Printf("a=%v (%T)\n", a, a) // a=5 (int)
fmt.Printf("b=%v (%T)\n", b, b) // b=5 (int64)
}
Ключевая мысль: тип получает переменная (a и b), а не константа. Константа просто «вписывается» в нужный тип, если значение представимо (помещается и не теряет смысл).
Контекст типа и default type
В какой-то момент компилятору всё равно нужно принять решение: если вы не указали тип, какой тип выбрать? Для untyped констант есть понятие default type (тип по умолчанию): он «проявляется» тогда, когда никакой другой типовой информации нет.
Например:
str := "Hello, 世界"
Работает так, будто вы написали var str string = "Hello, 世界". То есть untyped-константа «подсказывает» тип переменной, когда больше подсказок нет.
Таблица: типы по умолчанию
| Литерал/константа в коде | Пример | Тип по умолчанию (если больше ничего не известно) |
|---|---|---|
| целое число | |
|
| вещественное число | |
|
| строка | |
|
| булево | |
|
| руна (символ в одинарных кавычках) | |
rune (это int32) |
Для старта достаточно запомнить главное: целые → int, дробные → float64, если контекст не требует иного.
Нюанс: почему %T показывает int
Если вы напишете:
fmt.Printf("%T\n", 5)
вы увидите int. Но это не означает, что литерал 5 «внутри» всегда int. Это означает, что в данном контексте (передача аргумента) Go должен сформировать конкретное значение — и тогда используется default type.
Мини-пример:
package main
import "fmt"
func main() {
const n = 5 // untyped
const f = 1.25 // untyped
const s = "task" // untyped
fmt.Printf("%T\n", n) // int
fmt.Printf("%T\n", f) // float64
fmt.Printf("%T\n", s) // string
}
fmt.Printf фактически заставляет константу «выйти в мир значений», и она выходит с типом по умолчанию.
3. Typed константы и правила как у переменных
Теперь другой вариант:
const m int = 5
Это уже typed константа. У неё есть конкретный тип (int), и она подчиняется почти тем же правилам, что и обычные значения этого типа: если вы хотите использовать её как int64, компилятор ожидает явную конверсию.
Мини-пример: untyped можно «молча», typed — только явно
package main
import "fmt"
func main() {
const n = 5 // untyped
const m int = 5 // typed
var a int64 = n // ok
var b int64 = int64(m) // нужна конверсия
fmt.Println(a, b) // 5 5
}
Почему так строго? Потому что Go принципиально не смешивает типы «сам», и typed-константа — это «почти как переменная», только без права менять значение.
Untyped в выражениях: «приклеивается» к типу соседа
Это та ситуация, которая часто удивляет новичков: почему int64Var + 5 работает, а int64Var + typedIntConst — нет.
Смысл такой: untyped константа в выражении старается стать того типа, который нужен выражению, если это возможно. Typed-константа уже «выбрала сторону» и требует явного согласования.
package main
import "fmt"
func main() {
var x int64 = 10
const untyped = 5
const typed int = 5
fmt.Println(x + untyped) // 15
// fmt.Println(x + typed) // не компилируется
fmt.Println(x + int64(typed)) // 15
}
Тут есть логика: если значение уже typed (int), то смешивать int и int64 в одном выражении нельзя без явного решения программиста.
4. Представимость и проверка диапазона
Константные вычисления до выбора типа
Есть приятная тонкость: числовые untyped-константы живут в пространстве чисел «произвольной точности», и компилятор может выполнять с ними вычисления «как в математике», пока результат не нужно уложить в конкретный тип.
Практически это можно запомнить так:
- «Пока это константы» — вычислять можно довольно свободно.
- «Как только вы пытаетесь положить это в переменную» — включаются ограничения типа: диапазон, знаковость, точность.
Пример:
package main
import "fmt"
func main() {
const big = 1000
var x uint8 = big // не скомпилируется: 1000 не помещается в uint8
fmt.Println(big) // 1000
}
Компилятор заранее понимает: uint8 — это 0…255. Значит присваивание невозможно. И это хорошо: ошибка ловится сразу, а не «где-то в рантайме».
Проверка диапазона: компилятор как ранняя защита
Важно не перепутать поведение констант и переменных. У переменных иногда можно «силой» сделать конверсию (и получить переполнение), а для констант компилятор часто запрещает то, что выглядит как потенциально бессмысленное действие.
Простейшая проверка диапазона:
package main
import "fmt"
func main() {
const ok = 255
// const bad = 256
var b uint8 = ok
fmt.Println(b) // 255
}
Если вы попробуете использовать значение, не представимое в целевом типе, компилятор остановит вас сразу.
5. Практика: мини-CLI и валидация константами
Продолжим учебную линию «маленькое консольное приложение». Представим, что мы вводим «приоритет» задачи от 1 до 5.
Здесь цель простая: увидеть, где выгодно использовать untyped-константы, а где стоит сделать typed (чтобы компилятор помогал нам не ошибиться).
Untyped-константы как «правила», которые не мешают
package main
import "fmt"
func main() {
const minPriority = 1
const maxPriority = 5
var p int
fmt.Scan(&p)
ok := p >= minPriority && p <= maxPriority
fmt.Println(ok) // например: true
}
minPriority и maxPriority удобно оставить untyped: они нормально сравниваются с int, и вы не плодите конверсии.
Typed-константа как контракт на маленький тип
Иногда вы заранее хотите хранить число в маленьком типе, например uint8. В таком случае typed-константа фиксирует намерение: «это байт, и точка».
package main
import "fmt"
func main() {
const maxLen uint8 = 40
var title string
fmt.Scan(&title)
fmt.Println(len(title) <= int(maxLen)) // len(title) — это int
}
Да, здесь всё равно появляется int(maxLen), потому что len возвращает int. Но важнее другое: maxLen uint8 — это «контракт». Если вы попытаетесь поставить 300, компилятор не даст.
6. Схема: как выбирается тип untyped-константы
Иногда полезно держать в голове простую карту, чтобы не гадать, почему «в одном месте прокатило, в другом — нет».
flowchart TD
A[У нас есть константа: const x = 5] --> B{Есть явный тип?}
B -- да --> C[Typed constant: x имеет тип]
B -- нет --> D[Untyped constant: типа нет]
D --> E{Контекст требует конкретный тип?}
E -- да --> F[Подстроиться под нужный тип, если значение представимо]
E -- нет --> G[Использовать default type: int/float64/string/...]
C --> H[Как у переменных: нужен cast для другого типа]
F --> I[Если не представимо: compile error]
Ключевые слова: «контекст» и «представимо» (то есть значение реально помещается и остаётся осмысленным в целевом типе).
7. Типичные ошибки
Ошибка №1: «Я вывел %T и увидел int, значит константа всегда int».
Такое мышление появляется после первых экспериментов с fmt.Printf. На самом деле untyped-константа получает тип только тогда, когда это требуется контекстом, а печать через Printf как раз и создаёт этот контекст. Поэтому %T показывает не «истинную сущность константы», а тип значения, которое получилось в конкретной точке программы.
Ошибка №2: «Если это константа, она должна быть максимально строгой: типизируем всё подряд».
Это приводит к обратной проблеме: вы начинаете писать int64(x) и float64(y) там, где вообще-то хотели просто зафиксировать «5» и «60». Смысл untyped-констант — как раз в том, чтобы не заставлять вас делать конверсии в очевидных местах, сохраняя при этом строгую типизацию переменных.
Ошибка №3: «Почему int64Var + 5 работает, а int64Var + constTypedInt — нет? Компилятор сломался».
Компилятор не сломался: 5 — untyped и может «приклеиться» к int64, а typed-константа int — это уже конкретный тип, который нельзя смешивать с int64 без явного решения. Это тот же принцип, что и с переменными: Go не любит угадывать за программиста.
Ошибка №4: «Я сделаю const max uint8 = 300, а потом как-нибудь починю».
Не получится: компилятор не даст. Константы проверяются на представимость в типе на этапе компиляции, и это одна из их суперсил: вы ловите ошибку сразу, не дожидаясь странных переполнений.
Ошибка №5: «Раз константы такие гибкие, значит можно ими заменить ввод пользователя».
Константа не может зависеть от fmt.Scan и вообще от результатов выполнения программы. Если значение приходит извне (ввод, файл, сеть), это всегда переменная. Константы — это про правила и фиксированные значения, которые известны при компиляции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ