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("ласкаво просимо")
}

Тут 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 // нетипізована

	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 (тип за замовчуванням): він проявляється тоді, коли іншої типової інформації немає.

Наприклад:

str := "Hello, 世界"

Працює так, ніби ви написали var str string = "Hello, 世界". Тобто нетипізована константа підказує тип змінної, коли більше підказок немає.

Таблиця: типи за замовчуванням

Літерал/константа в коді Приклад Тип за замовчуванням (якщо більше нічого не відомо)
ціле число
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 має сформувати конкретне значення — і тоді використовується тип за замовчуванням.

Мініприклад:

package main

import "fmt"

func main() {
	const n = 5      // нетипізована
	const f = 1.25   // нетипізована
	const s = "завдання" // нетипізована

	fmt.Printf("%T\n", n) // int
	fmt.Printf("%T\n", f) // float64
	fmt.Printf("%T\n", s) // string
}

fmt.Printf фактично змушує константу стати значенням, і вона отримує тип за замовчуванням.

3. Типізовані константи й правила, як у змінних

Тепер інший варіант:

const m int = 5

Це вже типізована константа. У неї є конкретний тип (int), і вона підпорядковується майже тим самим правилам, що й звичайні значення цього типу: якщо ви хочете використати її як int64, компілятор очікує явне перетворення.

Мініприклад: нетипізовану можна використати «мовчки», типізовану — тільки явно

package main

import "fmt"

func main() {
	const n = 5     // нетипізована
	const m int = 5 // типізована

	var a int64 = n        // ок
	var b int64 = int64(m) // потрібне перетворення

	fmt.Println(a, b) // 5 5
}

Чому так суворо? Тому що Go принципово не змішує типи сам, а типізована константа — це майже як змінна, тільки без права змінювати значення.

Нетипізовані у виразах: підлаштовуються під тип сусіда

Це та ситуація, яка часто дивує новачків: чому int64Var + 5 працює, а int64Var + typedIntConst — ні.

Сенс такий: нетипізована константа у виразі намагається стати тим типом, який потрібен виразу, якщо це можливо. Типізована константа вже обрала сторону і вимагає явного узгодження.

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
}

Тут є логіка: якщо значення вже типізоване як int, то змішувати int і int64 в одному виразі не можна без явного рішення програміста.

4. Представлюваність і перевірка діапазону

Константні обчислення до вибору типу

Є приємна тонкість: числові нетипізовані константи живуть у просторі чисел довільної точності, і компілятор може виконувати з ними обчислення майже як у математиці, доки результат не потрібно вкласти в конкретний тип.

Практично це можна запамʼятати так:

  • «Поки це константи» — обчислювати можна доволі вільно.
  • «Щойно ви намагаєтеся покласти результат у змінну» — вмикаються обмеження типу: діапазон, знаковість, точність.

Приклад:

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.

Тут мета проста: побачити, де вигідно використовувати нетипізовані константи, а де краще зробити типізовані, щоб компілятор допомагав нам не помилятися.

Нетипізовані константи як «правила», які не заважають

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 зручно залишити нетипізованими: вони нормально порівнюються з int, і ви не плодите перетворення.

Типізована константа як контракт на маленький тип

Іноді ви заздалегідь хочете зберігати число в маленькому типі, наприклад uint8. У такому разі типізована константа фіксує намір: «це байт, і крапка».

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. Схема: як обирається тип нетипізованої константи

Іноді корисно тримати в голові просту карту, щоб не гадати, чому «в одному місці спрацювало, в іншому — ні».

flowchart TD
    A[У нас є константа: const x = 5] --> B{Чи є явний тип?}
    B -- так --> C[Типізована константа: x має тип]
    B -- ні --> D[Нетипізована константа: типу немає]
    D --> E{Контекст вимагає конкретний тип?}
    E -- так --> F[Підібрати потрібний тип, якщо значення представлюване]
    E -- ні --> G[Використати тип за замовчуванням: int/float64/string/...]
    C --> H[Як і зі змінними: для іншого типу потрібне явне перетворення]
    F --> I[Якщо значення непредставлюване — помилка компіляції]

Ключові слова: «контекст» і «представлюване» (тобто значення реально вміщується й лишається осмисленим у цільовому типі).

7. Типові помилки

Помилка №1: «Я вивів %T і побачив int, значить константа завжди int».
Таке мислення зʼявляється після перших експериментів із fmt.Printf. Насправді нетипізована константа отримує тип лише тоді, коли цього вимагає контекст, а друк через Printf якраз і створює цей контекст. Тому %T показує не «справжню сутність константи», а тип значення, який вийшов у цій конкретній точці програми.

Помилка №2: «Якщо це константа, вона має бути максимально суворою: типізуймо все підряд».
Це призводить до зворотної проблеми: ви починаєте писати int64(x) і float64(y) там, де насправді хотіли просто зафіксувати «5» і «60». Сенс нетипізованих констант якраз у тому, щоб не змушувати вас робити перетворення в очевидних місцях, зберігаючи при цьому сувору типізацію змінних.

Помилка №3: «Чому int64Var + 5 працює, а int64Var + constTypedInt — ні? Компілятор зламався».
Компілятор не зламався: 5 — нетипізована константа і може підлаштуватися під int64, а типізована константа int — це вже конкретний тип, який не можна змішувати з int64 без явного рішення. Це той самий принцип, що й зі змінними: Go не любить вгадувати за програміста.

Помилка №4: «Я зроблю const max uint8 = 300, а потім якось полагоджу».
Не вийде: компілятор не дозволить. Константи перевіряються на представлюваність у типі на етапі компіляції, і це одна з їхніх суперсил: ви ловите помилку одразу, не чекаючи дивних переповнень.

Помилка №5: «Якщо константи такі гнучкі, значить ними можна замінити введення користувача».
Константа не може залежати від fmt.Scan і взагалі від результатів виконання програми. Якщо значення приходить ззовні — з введення, файла чи мережі — це завжди змінна. Константи — це про правила й фіксовані значення, які відомі під час компіляції.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ