День 07 · Ввод и первые ошибки · страница 1 из 6

Программа читает ввод

До сих пор все данные жили внутри программы: цена, количество, остаток были записаны прямо в коде. Чтобы посчитать другой заказ, приходилось открывать main.go, менять число и запускать снова. Настоящие программы так не работают: кладовщик вводит количество с клавиатуры, склад присылает файл выгрузки, одна программа передаёт данные другой.

Сегодня программа научится получать данные снаружи. И сразу окажется, что снаружи приходит что угодно — в том числе не то, что ждали. Поэтому вторая половина дня — про ошибки: как их заметить, как о них сообщить и как завершить программу так, чтобы другие программы поняли, что что-то пошло не так.


Первая программа с вводом

Ситуация. На приёмке кладовщик пересчитал коробки и хочет ввести число. Программа должна дождаться ввода и что-то с ним сделать. Для начала — просто показать, что прочитала.

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Сколько штук пришло? ")
	scanner.Scan()
	line := scanner.Text()
	fmt.Println("Вы ввели:", line)
}

Что увидите. После go run . программа печатает вопрос и останавливается. Наберите 40 и нажмите Enter:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
Сколько штук пришло? 40
Вы ввели: 40

Цифры 40 после вопроса напечатала не программа: их набрали вы, а терминал показал набранное.

Как это читать.

Строка Что делает
import ( … ) три пакета: bufio — чтение ввода, fmt — печать, os — связь с операционной системой
scanner := bufio.NewScanner(os.Stdin) создать «читателя строк» scanner и подключить его к стандартному вводу os.Stdin — если ничего не менять, это клавиатура
fmt.Print("Сколько штук пришло? ") приглашение. Print, а не Println: курсор остаётся в той же строке, сразу после вопроса
scanner.Scan() прочитать одну строку: ждать, пока человек не нажмёт Enter
line := scanner.Text() взять прочитанную строку — без самого Enter. Тип у неё string

Новое в записи — точка после переменной: scanner.Scan(). До сих пор точка стояла после имени пакета: fmt.Println, strings.ToUpper. Здесь слева переменная scanner, а справа — действие, которое она умеет выполнять. Что это за переменные и откуда у них свои действия, разберём в блоке 5. Пока эти три строки — готовый рецепт: создать scanner, вызвать Scan(), забрать Text().

Почему нельзя сразу «прочитать число»? Всё, что приходит на ввод, — символы. Набрали 40 — пришли символ 4, символ 0 и перевод строки. Для программы это такая же строка, как сорок или 4О с буквой О. Превращать символы в число придётся отдельным шагом — на третьей странице. И этот шаг может не получиться.


Программа ждёт

Пока вы ничего не набрали, программа ничего не делает: она остановилась на scanner.Scan() и ждёт. Курсор мигает, приглашения терминала нет — это не зависание. Если программа не печатает вопрос, со стороны кажется, что не происходит вообще ничего. Поэтому первое, что стоит проверить, когда программа «молчит», — не ждёт ли она ввода.

Выйти из ожидания можно двумя способами.

Ctrl+D — «ввод закончен». Нажатый на пустой строке, он говорит программе, что ввода больше не будет: Scan() возвращается ни с чем, Text() отдаёт пустую строку, и программа идёт дальше:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
Сколько штук пришло? Вы ввели: 

Обе надписи оказались в одной строке: Enter вы не нажимали, поэтому на новую строку никто не перешёл.

Если в строке уже набраны символы, первое Ctrl+D отдаёт их программе, но ввод не заканчивает: Scan() ждёт либо перевод строки, либо конец ввода, и на экране ничего не меняется. Чтобы строка дошла до программы, Ctrl+D нажимают второй раз — об этом ещё сказано в таблице «Что может пойти не так» в конце страницы.

Ctrl+C — прервать программу. Она завершается сразу и до конца main не доходит:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
Сколько штук пришло? ^Csignal: interrupt

^C — так терминал показывает нажатие Ctrl+C. signal: interrupt дописывает go run: программу остановили сигналом прерывания. Тот же Ctrl+C вы знаете с дня 01 — он останавливает любую команду в терминале.


Попробуйте сейчас: первая программа с вводом.

Цель: увидеть три исхода ожидания ввода — строка с Enter, Ctrl+D и Ctrl+C.

1. Замените содержимое ~/gocourse/day07/scratch/main.go — наберите первую программу с вводом и сохраните:

✎ Наберите в файл scratch/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Сколько штук пришло? ")
	scanner.Scan()
	line := scanner.Text()
	fmt.Println("Вы ввели:", line)
}

2. Запустите три раза: наберите число и Enter; ничего не набирайте и нажмите Ctrl+D; запустите ещё раз и нажмите Ctrl+C.

▶ Выполните
cd ~/gocourse/day07/scratch
go run .

3. Потом запустите и наберите вместо числа слово сорок — программа напечатает и его: что лежит в строке, ей всё равно.

Готово, когда: после числа и Enter программа напечатала Вы ввели: с вашим числом; после Ctrl+D — Сколько штук пришло? Вы ввели: одной строкой; после Ctrl+C — ^Csignal: interrupt; на сорок — Вы ввели: сорок.


Две строки подряд

Ситуация. Кладовщик вводит название товара, потом склад, куда его приняли. Строк две.

Каждый вызов scanner.Scan() читает следующую строку. Две строки — два вызова. scanner при этом один на всю программу.

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Товар: ")
	scanner.Scan()
	item := scanner.Text()
	fmt.Print("Склад: ")
	scanner.Scan()
	store := scanner.Text()
	fmt.Println("Принято:", item, "на склад", store)
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
Товар: Болт М6
Склад: Север
Принято: Болт М6 на склад Север

Как это читать. Строка — это всё, что набрано до Enter, вместе с пробелами: Болт М6 пришло целиком, одной строкой, а не двумя словами. Порядок работы такой: вопрос, ожидание, первая строка в item; вопрос, ожидание, вторая строка в store; печать.

Если после первой строки нажать Ctrl+D, второй Scan() ничего не прочитает, и store останется пустой:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
Товар: Болт М6
Склад: Принято: Болт М6 на склад 

Программа при этом не упала и не пожаловалась — просто напечатала пустое место. Для склада это плохо: товар «принят» неизвестно куда. Как заметить, что ввода не было, — на следующей странице.


Что может пойти не так

Что видите Что это значит Что делать
undefined: bufio пакет не подключён добавить "bufio" в import ( … )
undefined: os то же для os: os.Stdin есть, а "os" в import нет добавить "os"
"bufio" imported and not used пакет подключён, а bufio. в коде нигде нет убрать строку из import или дописать код, который его использует
scanner.Scan (value of type func() bool) is not used scanner.Scan без скобок: действие названо, но не выполнено scanner.Scan()
not enough arguments in call to bufio.NewScanner и ещё две строки: have (), want (io.Reader) bufio.NewScanner() без аргумента: не сказано, откуда читать bufio.NewScanner(os.Stdin)
программа собралась и напечатала что-то вроде Вы ввели: 0x… — адрес в памяти line := scanner.Text без скобок: в line попала не строка, а само действие, и печатается его адрес scanner.Text(). go vet . находит такое заранее: fmt.Println arg <имя> is a func value, not called
программа не ждёт и печатает пустую строку нет scanner.Scan() перед Text(): строку никто не прочитал Scan() перед каждым Text()
программа ничего не печатает, приглашение терминала не возвращается программа ждёт ввода набрать строку и Enter; Ctrl+D — закончить ввод; Ctrl+C — прервать
набрали 40, нажали Ctrl+D — ничего не произошло в строке уже есть символы: первое Ctrl+D отдаёт их программе, но ввод не заканчивает, а Scan() ждёт либо перевод строки, либо конец ввода нажать Ctrl+D ещё раз. Тогда программа напечатает Вы ввели: 40 вплотную к набранному 40, без перевода строки: Enter не нажимали, переводить нечего
терминал закрылся после Ctrl+D программа уже закончилась, вернулось приглашение, и Ctrl+D получил сам терминал: для него это «работа окончена» Ctrl+Shift+ — новый терминал, cd ~/gocourse/day07/scratch` обратно. Ctrl+D нажимать, только пока программа ждёт ввода

Две строки have () и want (io.Reader) читаются так: «передано ничего, а нужно одно значение — то, из чего можно читать». os.Stdin как раз такое значение.


Попробуйте сейчас: три поломки программы с вводом.

Цель: увидеть сообщения о трёх частых поломках и записать их в словарик.

Наберите в scratch/main.go программу «Товар / Склад» из раздела «Две строки подряд»:

✎ Наберите в файл scratch/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Товар: ")
	scanner.Scan()
	item := scanner.Text()
	fmt.Print("Склад: ")
	scanner.Scan()
	store := scanner.Text()
	fmt.Println("Принято:", item, "на склад", store)
}

Сломайте её тремя способами — по одному за раз, запуская go run . после каждого:

  1. удалите строку "bufio" из import;
  2. уберите скобки у первого scanner.Scan();
  3. уберите скобки у scanner.Text() в строке с item; запустите и наберите две строки, потом выполните go vet ..

После каждой поломки:

▶ Выполните
cd ~/gocourse/day07/scratch
go run .

После третьей — ещё:

▶ Выполните
go vet .

После каждой поломки верните как было. Каждое новое сообщение — строкой в ~/errors.md: текст ошибки, тире, что это значило.

Готово, когда: вы видели undefined: bufio, scanner.Scan (value of type func() bool) is not used, адрес вместо товара в выводе и находку go vet … is a func value, not called; программа снова как в «Двух строках», сообщения записаны в ~/errors.md.


Дальше: ввод из конвейера и файла — course next