JavaRush /Курсы /Go SELF /Константы в Go: с типом и без

Константы в Go: с типом и без

Go SELF
8 уровень , 1 лекция
Открыта

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-константа «подсказывает» тип переменной, когда больше подсказок нет.

Таблица: типы по умолчанию

Литерал/константа в коде Пример Тип по умолчанию (если больше ничего не известно)
целое число
const x = 10
int
вещественное число
const x = 1.5
float64
строка
const s = "go"
string
булево
const b = true
bool
руна (символ в одинарных кавычках)
const r = 'A'
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 и вообще от результатов выполнения программы. Если значение приходит извне (ввод, файл, сеть), это всегда переменная. Константы — это про правила и фиксированные значения, которые известны при компиляции.

1
Задача
Go SELF, 8 уровень, 1 лекция
Недоступна
Паспорт констант
Паспорт констант
1
Задача
Go SELF, 8 уровень, 1 лекция
Недоступна
Плюс пять
Плюс пять
1
Задача
Go SELF, 8 уровень, 1 лекция
Недоступна
Фейс контроль
Фейс контроль
1
Задача
Go SELF, 8 уровень, 1 лекция
Недоступна
Диски и блоки
Диски и блоки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ