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("Answer:", result) — программа выглядит умнее. Но проверяющая система обычно ждёт строго result, без префиксов. Поэтому на этапе print держите в голове правило: выводим только то, что просит условие, и ровно в том формате, который просит условие.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
fmt.Println(10 / 0)дляint? Посколько оба числа указаны явно сразу в выражении, то компилятор сразу видит проблему "деления на ноль" и прерывает сборку программы. А значит, правильным ответом будет "ошибка компиляции". Проверил локально.