1. Вступ
Коли ви тільки починаєте розв’язувати задачі, дуже легко перетворити main() на кашу: половина рядків читає вхід, чверть щось рахує, ще трохи друкує, а посередині раптом конкатенація рядків і спроба помножити текст на число. Спойлер: Go цього не оцінить. Шаблон read → parse → compute → print потрібен для того, щоб ваш код був передбачуваним: ви завжди розумієте, на якому етапі перебуваєте, і де шукати помилку, якщо результат не збігається з очікуваним.
Найприємніше в цьому шаблоні — те, що він підходить майже до всіх задач зі стандартним введенням і виведенням. Навіть якщо задача проста, звичка розкладати все по етапах економить нерви. А нерви, як відомо, у програмуванні — витратний матеріал. Тож добре, коли витрата помірна.
Загальна схема: що означає кожен крок
Якщо описати шаблон людською мовою, вийде буквально так: «прочитав → зрозумів → порахував → показав результат». Звучить як звичайна розмова, і це добре: програмування для новачка має бути максимально розмовним, без відчуття, що ви викликаєте давніх духів.
Ось схема, яку корисно тримати в голові, а іноді й прямо писати в коментарях у коді:
flowchart LR
A[read: читаємо вхідні дані] --> B[parse: за потреби перетворюємо типи]
B --> C[compute: обчислюємо результат]
C --> D[print: виводимо результат у потрібному форматі]
І ось невелика таблиця: що зазвичай роблять на кожному етапі.
| Етап | Що робимо | Типовий результат етапу |
|---|---|---|
|
зчитуємо дані в змінні | a, b, s, n отримали значення |
|
перетворюємо рядок на число або навпаки | n := atoi(s) або text := itoa(n) |
|
виконуємо обчислення | sum, area, change, result |
|
друкуємо у потрібному форматі | один рядок, одне число або кілька значень |
Важливо: крок parse потрібен не завжди. Якщо вхід уже є числом і ми читаємо його в int, тоді парсити нічого не потрібно. Але якщо число надходить у вигляді тексту, без Atoi не обійтися.
2. Крок read: читаємо вхід так, щоб далі було зручно
На етапі read ми виконуємо найтиповішу роботу: оголошуємо змінні й зчитуємо в них значення. Тут часто виникає спокуса одразу щось порахувати просто всередині Scan, але краще не поспішати: спочатку дістаньмо дані, а потім уже думатимемо.
Найпростіший варіант — читаємо числа одразу в int:
package main
import "fmt"
func main() {
var a, b int
fmt.Scan(&a, &b) // read
fmt.Println(a + b) // compute+print (поки в один рядок)
}
Цей приклад робочий, але в ньому етапи зведені докупи. Для тренування шаблону краще розділяти їх, навіть якщо здається: «Та ну, тут же 2 рядки».
Ось та сама думка, але акуратніше:
package main
import "fmt"
func main() {
// read
var a, b int
fmt.Scan(&a, &b)
// compute
sum := a + b
// print
fmt.Println(sum)
}
Порожні рядки й коментарі тут — не прикраса. Це візуальні межі. Потім ви будете вдячні собі, коли почнете помилятися в довших задачах. Тобто приблизно завтра. Або, точніше, в якийсь момент.
3. Крок parse: коли вхід текстовий, а рахувати треба числа
На етапі parse ми перетворюємо один тип даних на інший. І тут важливо прийняти філософію Go: «само не перетвориться». Спочатку це дратує, але потім раптом стає зрозуміло, чому так безпечніше: ви не можете випадково скласти «12» і 3 та отримати «123» або 15. Ви зобов’язані явно обрати потрібний варіант.
Припустімо, за умовою задачі вам дають число як рядок. Це трапляється частіше, ніж здається: коди, номери, іноді «0012» тощо. Тоді read читає рядок, а parse перетворює його на int:
package main
import (
"fmt"
"strconv"
)
func main() {
// read
var s string
fmt.Scan(&s)
// parse
n, _ := strconv.Atoi(s)
// compute
double := n * 2
// print
fmt.Println(double)
}
Тут ми використали _, щоб ігнорувати помилку. У навчальних задачах це інколи допустимо, бо формат введення гарантований: якщо написано «на вхід подається ціле число», значить, так і буде. Але важливо розуміти: Atoi повертає (int, error), а error — це звичайне значення, яке можна перевіряти й обробляти. У Go прийнято явно працювати з помилками як зі значеннями.
Якщо ви хочете хоча б побачити, чи сталася помилка, без розгалужень, які ми ще не використовуємо, просто виведіть err:
package main
import (
"fmt"
"strconv"
)
func main() {
var s string
fmt.Scan(&s)
n, err := strconv.Atoi(s)
fmt.Println(n) // якщо є помилка, n буде 0
fmt.Println(err) // <nil>, якщо все добре
}
Так, це не красива обробка. Але для розуміння механіки — чудово.
4. Крок compute: рахуємо результат і не змішуємо етапи
Етап compute — це місце, де ви розв’язуєте задачу. Тут дуже корисно триматися правила: «одна змінна — одна ідея». Новачки часто пишуть величезну формулу просто всередині Println, а потім губляться: помилка в арифметиці, у пріоритеті операцій чи взагалі в тому, що ввід був не той.
Замість цього краще так: проміжні значення — у змінні з нормальними іменами.
Наприклад, нехай у нас є задача: дані ширина й висота прямокутника, треба вивести площу та периметр.
package main
import "fmt"
func main() {
// read
var width, height int
fmt.Scan(&width, &height)
// compute
area := width * height
perimeter := 2*width + 2*height
// print
fmt.Println(area, perimeter)
}
Зверніть увагу: perimeter := 2*width + 2*height читається легко, бо ми не намагаємося робити математику на швидкість. Якщо хочеться ще ясніше, можна додати дужки, навіть коли вони не потрібні. Іноді це цілком нормальна плата за читабельність.
5. Крок print: виведення — це частина розв’язання, а не «ну потім якось»
Виведення в навчальних задачах — підступна річ. Можна ідеально порахувати відповідь, але вивести «Відповідь: 42» замість «42» — і отримати неправильне рішення. Програма не зобов’язана бути ввічливою. Їй треба бути точною.
На етапі print корисно пам’ятати кілька речей.
fmt.Println друкує аргументи через пробіл і додає переведення рядка. fmt.Print друкує без переведення рядка. Якщо ви виводите кілька чисел, Println — найпростіший шлях.
package main
import "fmt"
func main() {
x := 5
y := 7
fmt.Println(x, y) // 5 7
}
Якщо вам потрібно зібрати рядок вручну, наприклад «x=5», тоді або друкуйте аргументами, або використовуйте strconv.Itoa. Конкатенація рядків із числами напряму не спрацює.
package main
import (
"fmt"
"strconv"
)
func main() {
x := 5
line := "x=" + strconv.Itoa(x)
fmt.Println(line) // x=5
}
6. Міні‑приклад: «Каса»
Зараз зберемо маленький єдиний застосунок, який ідеально лягає на шаблон: міні‑«каса». Ідея проста: на вхід подається вартість і сума оплати (у центах), а на вихід — решта, теж у центах, а потім — розкладена на долари й центи. Умови тут нам поки не потрібні, тому вважаємо, що оплата завжди не менша за вартість.
Почнемо з версії, де на вхід уже надходять числа (int). Це прямолінійно й приємно.
package main
import "fmt"
func main() {
// read
var priceCents, paidCents int
fmt.Scan(&priceCents, &paidCents)
// compute
changeCents := paidCents - priceCents
changeDollars := changeCents / 100
changeRestCents := changeCents % 100
// print
fmt.Println(changeDollars, changeRestCents)
}
Тепер трохи ускладнимо життя: нехай ціна й оплата надходять як рядки. Це поширена ситуація, коли дані схожі на число, але зберігаються у текстовому вигляді.
package main
import (
"fmt"
"strconv"
)
func main() {
// read
var priceStr, paidStr string
fmt.Scan(&priceStr, &paidStr)
// parse
priceCents, _ := strconv.Atoi(priceStr)
paidCents, _ := strconv.Atoi(paidStr)
// compute
changeCents := paidCents - priceCents
dollars := changeCents / 100
cents := changeCents % 100
// print
fmt.Println(dollars, cents)
}
Зверніть увагу, як передбачувано це читається: ви буквально бачите, де ввід, де парсинг, де математика, де виведення. Це і є головна цінність шаблону.
7. Як називати змінні, щоб не плутатися
Хороші імена змінних — це коли ви можете прочитати код як історію. Погані — коли код перетворюється на x1, x2, tmp, res2, і ви починаєте вгадувати: tmp — це тимчасово чи назавжди? Чому res2, а де res1? А x — це ширина чи вік кота?
Домовленість, яка добре працює в задачах із введенням і виведенням: вхідні змінні називаємо за змістом, а вихідні — за роллю.
Наприклад, порівняйте два варіанти. Формально обидва працюють, але мозок сприймає їх по-різному.
package main
import "fmt"
func main() {
var a, b int
fmt.Scan(&a, &b)
c := a*b + a + b
fmt.Println(c)
}
А тепер так:
package main
import "fmt"
func main() {
var width, height int
fmt.Scan(&width, &height)
area := width * height
perimeter := 2*width + 2*height
fmt.Println(area, perimeter)
}
У другому випадку навіть людина, яка не писала цю програму, розуміє, що відбувається. І це важливіше, ніж здається: через кілька годин ви самі вже можете бути не тією людиною, яка писала код. Пам’ять любить відпустку.
Нижче — маленька підказка-таблиця: як часто зручно називати змінні в навчальних задачах.
| Зміст | Приклади імен |
|---|---|
| розміри | |
| кількість | |
| сума | |
| відповідь | |
| вхідний рядок | |
| число після парсингу | |
Зверніть увагу: інколи n — цілком нормальне ім’я. Але лише тоді, коли воно справді означає кількість або розмір, а не коли ви просто втомилися вигадувати назви.
8. Мікродіагностика: друк проміжних значень
Коли ви розв’язуєте задачу й отримуєте не той результат, дуже хочеться одразу впасти в містику: «комп’ютер зламався», «у Go баг», «всесвіт проти мене». На практиці найчастіше проблема в одному з етапів: не так прочитали, не так розпарсили, не так порахували або не так вивели.
Поки ми ще не використовуємо розгалуження, найкращий спосіб налагодження — тимчасово друкувати проміжні значення. Причому так, щоб потім їх легко було прибрати.
Наприклад, у «касі»:
package main
import "fmt"
func main() {
var priceCents, paidCents int
fmt.Scan(&priceCents, &paidCents)
changeCents := paidCents - priceCents
fmt.Println(priceCents, paidCents, changeCents) // тимчасове налагодження
dollars := changeCents / 100
cents := changeCents % 100
fmt.Println(dollars, cents)
}
Так, це ламає формат виведення для перевіряльної системи. Але налагодження й фінальна версія — це різні стани коду. Головне — не забути прибрати налагоджувальні рядки перед здачею. Це вічна класика.
9. Типові помилки
Помилка №1: змішування етапів до стану «незрозуміло, де що».
Початківець часто пише: fmt.Scan(&a); fmt.Println("ans", a+something) — і так по всій програмі. Нібито працює, доки задача маленька. Але щойно з’являється парсинг рядка, кілька входів або пара проміжних обчислень, код перетворюється на клубок. Лікується дисципліною: спочатку read, потім, якщо потрібно, parse, далі compute, а потім print. Між ними — порожні рядки.
Помилка №2: спроба склеїти рядок і число оператором +.
У Go оператор + або додає числа, або склеює рядки. Він не робить «і те, і інше за настроєм». Тому "age=" + age не скомпілюється. Якщо потрібно вивести разом, друкуйте аргументами (fmt.Println("age=", age)) або перетворюйте число на рядок через strconv.Itoa.
Помилка №3: парсинг роблять про всяк випадок, навіть коли він не потрібен.
Іноді студент уже дізнався про Atoi і починає використовувати його завжди: читає int через Scan, а потім думає: «А чи не розпарсити ще раз?» Це зайве й тільки плутає. Якщо вхід — число, читайте його в int і одразу рахуйте. parse потрібен лише тоді, коли тип входу не збігається з типом, з яким ви хочете працювати.
Помилка №4: імена змінних на кшталт x, x1, x2, tmp, коли зміст важливий.
Короткі імена допустимі, але вони не мають перетворювати задачу на шифрування. Якщо величина — ширина, нехай буде width. Якщо це вартість у центах, нехай буде priceCents. Такі імена зменшують кількість логічних помилок: ви рідше переплутаєте величини місцями, бо вони виглядають по-різному.
Помилка №5: неправильне виведення через «дружні слова».
Дуже хочеться писати fmt.Println("Відповідь:", result) — програма виглядає розумнішою. Але перевіряльна система зазвичай очікує строго result, без префіксів. Тому на етапі print тримайте в голові просте правило: виводимо тільки те, що просить умова, і рівно в тому форматі, який просить умова.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ