1. Введение
Когда вы пишете первые программы, они часто похожи на музыкальную шкатулку: открыл — играет одну и ту же мелодию. Это нормально на старте, но довольно быстро хочется, чтобы программа реагировала на пользователя или на входные данные из задачи: «введи два числа — получи сумму», «введи имя — получи приветствие».
В Go (и в большинстве учебных задач) основной путь — читать значения из стандартного ввода (stdin) и писать результат в стандартный вывод (stdout).
Главная цель этой лекции — сделать ввод предсказуемым. Чтобы вы точно понимали, сколько значений вы читаете, почему перед переменной ставится &, и что означают два возвращаемых результата у Scan/Fscan.
Как Scan видит ввод: пробелы и переводы строк — это разделители
Когда вы смотрите на входные данные глазами человека, вы видите «строчку», «абзац», «переносы». Когда на них смотрит fmt.Scan, он видит не «строчки», а последовательность значений, разделённых пробелами и переводами строки.
То есть для Scan обычно нет большой разницы, было ли 10 20 в одной строке или 10 на одной строке и 20 на следующей — это всё равно два значения.
flowchart LR
A[stdin: '10 20 Bob'] --> B[fmt.Scan / fmt.Fscan]
B --> C[токен 1: 10]
B --> D[токен 2: 20]
B --> E[токен 3: Bob]
Для новичка это очень удобная модель: вы заранее знаете «мне нужно 3 значения» — значит, вы зовёте Scan так, чтобы он попытался прочитать ровно 3 значения (или делаете несколько вызовов).
fmt.Scan: читаем одно значение
Давайте начнём с самого спокойного варианта: вводим одно целое число и печатаем его обратно. Это упражнение полезно не потому, что кому-то нужен «эхо‑бот», а потому что оно фиксирует механику: переменная должна существовать заранее, и Scan должен получить “куда записать”.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
fmt.Println(n) // если ввели 7, то выведет: 7
}
Здесь важно две вещи.
Первая: мы объявили n заранее через var n int. Ввод не создаёт переменные, он заполняет уже существующие.
Вторая: мы написали fmt.Scan(&n), а не fmt.Scan(n). И вот это & сейчас самый частый вопрос на всём старте Go.
Важные нюансы про &x
Если объяснять совсем без теории указателей (и мы так и сделаем), то &n можно читать так: «передаю функции место, куда она должна записать ответ».
Представьте, что переменная — это коробка с наклейкой n. Если вы вызываете fmt.Scan(n), вы как будто говорите: «Вот что лежит в коробке сейчас». Но Scan не нужен текущий предмет из коробки, ему нужна возможность положить новый предмет внутрь. Поэтому вы передаёте не содержимое коробки, а её “адрес”, то есть саму коробку: &n.
Немного более технически (но всё ещё “по‑бытовому”): &n — это адрес переменной n в памяти. Go не позволяет Scan возвращать значение напрямую через параметры, поэтому он пишет в переменную через адрес.
Если сделать ошибку и забыть &, компилятор обычно скажет что-то в духе: «нельзя использовать n (переменная типа int) как аргумент… ожидается указатель». Это тот случай, когда компилятор ворчит по делу: он буквально говорит «ты не дал мне коробку, ты дал мне число».
Читаем несколько значений за один раз
Очень типичный формат задач: «даны два целых числа a и b». Тогда ввод выглядит как a b (или на разных строках), и нам удобно считать сразу оба:
package main
import "fmt"
func main() {
var a, b int
fmt.Scan(&a, &b)
sum := a + b
fmt.Println(sum) // если ввели 10 3, то выведет: 13
}
Здесь Scan попытается прочитать ровно два значения: сначала в a, потом в b. Поэтому порядок переменных должен совпадать с порядком значений во входе.
Если вам дали вход 3 10, а вы сделали fmt.Scan(&b, &a), программа не “сломается” — она честно прочитает 3 в b, 10 в a. Сломается только ваше ожидание результата (а это иногда больнее).
2. Что возвращает Scan: (count, err)
Два результата: сколько считали и была ли ошибка
Многие функции Go возвращают не одно значение, а два (или больше). У fmt.Scan ровно такой случай: он возвращает количество успешно считанных значений и ошибку, если что-то пошло не так.
Почему так сделали в Go? Потому что в Go ошибки — это значения, с которыми работают явно: их можно вернуть, сохранить, напечатать, сравнить с nil. Этот подход известен как “errors are values”.
Посмотрим на это «в лоб», без умных обработок: просто считаем число и печатаем, что вернул Scan.
package main
import "fmt"
func main() {
var x int
count, err := fmt.Scan(&x)
fmt.Println("count:", count) // например: count: 1
fmt.Println("err:", err) // при успехе: err: <nil>
fmt.Println("x:", x) // если ввели 42, то: x: 42
}
Как это читать.
count — это сколько значений реально удалось распознать и записать в переменные. Если вы просили одно значение, то при нормальном вводе count будет 1.
err — это ошибка. Если ошибки нет, err равен nil. Это важная деталь: nil означает “ничего нет”, в данном случае — “ошибки нет”.
Здесь мы не строим сложную логику “что делать при ошибке” (это будет позже, когда вы познакомитесь с условными операторами), но уже сейчас полезно научиться хотя бы понимать диагностику и не пугаться <nil>: <nil> — это хорошая новость.
Почему count иногда меньше, чем вы ожидали
Бывает ситуация: вы написали fmt.Scan(&a, &b, &c), ожидая три значения, но во входе дали только два. Или вместо числа пришло слово. Тогда Scan не может корректно заполнить все переменные.
В такой момент (обычно) происходит следующее: count будет показывать, сколько значений успели считать, а err станет не nil (то есть будет какая-то ошибка).
Давайте смоделируем чтение двух чисел, но представим, что во входе внезапно оказалось “10 котик”.
package main
import "fmt"
func main() {
var a, b int
count, err := fmt.Scan(&a, &b)
fmt.Println("count:", count) // вероятно: count: 1
fmt.Println("err:", err) // будет не <nil>, там описание ошибки
fmt.Println("a:", a) // a: 10
fmt.Println("b:", b) // b: 0 (останется нулевым значением)
}
Почему b стал 0? Потому что b успели объявить (var b int), а int по умолчанию равен нулю, и если ввод не смог записать новое значение — остаётся “как было”.
И ещё раз подчеркну: вы сейчас не обязаны строить обработку ошибок “правильно”. Но вы уже можете понимать поведение: “часть ввода считалась, потом случилась ошибка”.
Blank identifier _: когда вы не хотите хранить один из результатов
Иногда вам не нужен count. В большинстве задач формата “ввод гарантирован корректный” вы действительно не используете count, потому что ожидаете, что всё прочитается нормально.
В Go принято явно показывать: «я осознанно игнорирую этот результат». Для этого есть специальное имя _ (подчёркивание), его называют blank identifier.
package main
import "fmt"
func main() {
var n int
_, err := fmt.Scan(&n)
fmt.Println("err:", err) // при успехе: err: <nil>
fmt.Println(n) // например: 5
}
Почему это полезно, даже если вы пока не обрабатываете ошибки? Потому что код становится честнее: вы не делаете вид, что count важен, и не плодите переменные “на всякий случай”.
3. Паника в Go
Иногда программа в Go не “возвращает ошибку”, а аварийно прекращает работу. Это называется panic.
Паника — это сигнал: “произошло что-то настолько неправильное, что продолжать выполнение опасно или бессмысленно”. Чаще всего panic возникает из-за багов в коде или серьезных сбоев в данных.
Важно не путать:
- err — это обычная ошибка, с ней можно работать: проверить, вывести сообщение, вернуть наверх.
- panic — это падение программы: Go печатает сообщение об ошибке, место где она произошла, и выполнение останавливается.
Пример типичной паники, которую новички встретят довольно рано:
package main
import "fmt"
func main() {
x := 10
y := 0
fmt.Println(x / y) // panic: runtime error: integer divide by zero
}
Если вы знакомы с концепцией исключений, то panic похож на exception, только используется при сильных сбоях.
Поэтому если программа внезапно упала и вы видите слово panic, это почти всегда означает ошибку логики (или неверные предположения о данных). А вот fmt.Scan обычно не паникует — он возвращает err, чтобы вы могли обработать ситуацию спокойно.
4. fmt.Fscan: ввод с явным источником os.Stdin
fmt.Scan удобен тем, что он читает из стандартного ввода “сам”. Но иногда вам нужно чуть больше контроля: например, вы хотите читать не из stdin, а из другого источника (файла, строки, сети). Тогда используется fmt.Fscan, где первая вещь — это откуда читать.
На нашем текущем уровне нам достаточно познакомиться с самым простым вариантом: os.Stdin — это тот же стандартный ввод, просто переданный явно.
package main
import (
"fmt"
"os"
)
func main() {
var a, b int
fmt.Fscan(os.Stdin, &a, &b)
fmt.Println(a * b) // если ввели 4 5, то выведет: 20
}
С практической точки зрения в учебных задачах Scan и Fscan(os.Stdin, ...) часто взаимозаменяемы. Но “идея” Fscan важна: функция ввода может читать не только “из консоли”, и в Go это делается через явную передачу источника.
Если вы видите в чужом решении fmt.Fscan(...), не пугайтесь: это всё тот же Scan, только с уточнением “читать вот отсюда”.
5. Мини‑приложение: «Калькулятор счёта»
Чтобы закрепить ввод в контексте нормальной программы, продолжим наш маленький учебный проект (пусть он будет называться calcbox). До этого он умел только печатать и считать на заранее заданных значениях. Теперь он будет читать числа из ввода и делать вычисление.
Сценарий простой: вводим цену и количество, печатаем итоговую стоимость. Никаких процентов и строк‑чисел — всё целочисленно, чтобы не смешивать темы.
package main
import "fmt"
func main() {
// read
var price int
var qty int
fmt.Scan(&price, &qty)
// compute
total := price * qty
// print
fmt.Println(total) // если ввели 120 3, то выведет: 360
}
Обратите внимание, как органично сюда легли правила из прошлых лекций. Переменные объявлены заранее, ввод идёт через &, вычисление — отдельной строкой, вывод — в конце. Даже если программа маленькая, такой порядок спасает мозг от перегрева.
Если захотите сделать отладочный режим “посмотреть, что прочиталось”, вы уже умеете временно добавить печать count и err, а потом убрать.
6. Типичные ошибки при вводе через fmt.Scan и fmt.Fscan
Ошибка №1: забыли & перед переменной.
Это самая частая история. Вы пишете fmt.Scan(n) и ожидаете, что n заполнится, но компилятор ругается. Причина простая: Scan должен куда-то записать значение, а без & вы передали не “место”, а текущее содержимое. Думайте “передаю коробку, а не предмет в коробке”.
Ошибка №2: переменная не объявлена до ввода.
Иногда хочется написать что-то вроде fmt.Scan(&x) без предварительного var x int. Но функция не может записать в “переменную, которой нет”. Сначала объявляем, потом сканируем. В начале лучше писать чуть более многословно, зато без магии.
Ошибка №3: перепутан порядок чтения.
fmt.Scan(&a, &b) читает сначала a, потом b. Если во входе порядок другой, результат будет “не тот”, хотя программа формально работает. Это неприятный класс ошибок: код компилируется, запускается, но ответ неверный. Лечится вниманием к формату входных данных и говорящими именами переменных.
Ошибка №4: ожидали, что Scan прочитает строку целиком вместе с пробелами.
Scan читает токены, разделённые пробелами. Если вы вводите "John Smith", то в string через Scan попадёт только "John". Это не баг, это правило. Просто сейчас мы работаем с “словами” и “числами”, а не с “целыми строками текста”.
Ошибка №5: игнорируют (count, err) и потом долго не понимают, почему переменная осталась нулевой.
Да, по условиям многих задач ввод гарантирован корректный. Но когда вы отлаживаете свой код или случайно вводите не то, err — единственная подсказка, что произошло. Подход “ошибки — это значения” как раз и существует, чтобы их можно было увидеть и понять.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ