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, 世界". Тобто нетипізована константа підказує тип змінної, коли більше підказок немає.
Таблиця: типи за замовчуванням
| Літерал/константа в коді | Приклад | Тип за замовчуванням (якщо більше нічого не відомо) |
|---|---|---|
| ціле число | |
|
| дійсне число | |
|
| рядок | |
|
| булеве значення | |
|
| руна (символ в одинарних лапках) | |
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 і взагалі від результатів виконання програми. Якщо значення приходить ззовні — з введення, файла чи мережі — це завжди змінна. Константи — це про правила й фіксовані значення, які відомі під час компіляції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ