День 08 · дополнительные материалы

День 08 — глубже

Всё здесь необязательно. Материалы на английском помечены.


Тур по Go: цикл for (англ.)

Что говорит спецификация языка (англ.)

Effective Go: раздел For (англ.)

Ошибка на единицу (англ.)

Документация к Scanner прямо в терминале

Попробуйте сейчас: документация к Scanner.

Цель: узнать из go doc, что возвращает Scan, когда ввод кончился или случилась ошибка чтения.

▶ Выполните
go doc bufio.Scanner.Scan
go doc bufio.Scanner.Text

go doc вы знаете с дня 04. Прочитайте, что Scan возвращает, когда ввод кончился или случилась ошибка чтения, и сравните с тем, как это описано на странице 5.

Готово, когда: вы можете своими словами сказать, что вернёт Scan в обоих случаях, и найти это место на странице 5.

Чужой синтаксис заголовка

В других языках цикл пишут иначе, и руки по привычке пишут чужой синтаксис. Два случая дают сразу по две строки сообщений. ++i вместо i++ — в Go такой записи нет:

Код для чтения · разбираем, набирать не нужно
	for i := 0; i < 3; ++i {
		fmt.Println(i)
	}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
# day08/scratch
./main.go:6:21: syntax error: unexpected ++, expected expression
./main.go:7:16: syntax error: unexpected ), expected { after for clause

i++ внутри вызова — в Go это отдельная команда, а не выражение со значением:

Код для чтения · разбираем, набирать не нужно
	for i := 0; i < 3; {
		fmt.Println("Коробка", i++)
	}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
# day08/scratch
./main.go:7:34: syntax error: unexpected ++ in argument list; possibly missing comma or )

Полная таблица сообщений про заголовок (номер строки и колонки у вас будут свои, текст — тот же):

Что видите Что это значит Что делать
syntax error: unexpected :=, expected ) заголовок в круглых скобках: for (i := 0; i < 3; i++) убрать круглые скобки
syntax error: unexpected newline, expected { after for clause { перенесена на следующую строку или забыта { в конце строки с for
syntax error: unexpected ++, expected { части разделены запятыми: for i := 0, i < 3, i++ между частями — ;
syntax error: unexpected ++, expected { after for clause третья часть i = i++ просто i++
unexpected ++, expected expression и вторая строка unexpected ), expected { after for clause ++i вместо i++ i++
unexpected ++ in argument list; possibly missing comma or ) i++ внутри Println(…) i++ отдельной строкой

А while компилятор принимает за имя переменной — отсюда две строки сообщений со страницы 3:

Код для чтения · разбираем, набирать не нужно
	stock := 30
	while stock > 0 {
		stock -= 12
	}
	fmt.Println(stock)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
# day08/scratch
./main.go:7:8: syntax error: unexpected name stock at end of statement
./main.go:10:2: syntax error: non-declaration statement outside function body

Вторая строка — следствие первой.

Четыре заголовка при n := 5

Попробуйте сейчас: четыре заголовка при n := 5.

Цель: до запуска назвать для каждого цикла первое значение, последнее и число проходов — и сверить с выводом.

1. Наберите в scratch/main.go программу со страницы 2, но с n := 5:

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

import "fmt"

func main() {
	n := 5
	for i := 0; i < n; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println("— i := 0; i < n")
	for i := 1; i <= n; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println("— i := 1; i <= n")
	for i := 1; i < n; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println("— i := 1; i < n")
	for i := 0; i <= n; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println("— i := 0; i <= n")
}

2. До запуска запишите для каждого цикла первое значение, последнее и число проходов.

3. Запустите и сверьте:

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

Готово, когда: в каждой из четырёх строк вывода первое и последнее число и их количество совпали с записанными.

Вложенные циклы

Ситуация. Стеллаж: три полки, на каждой четыре ячейки. Нужно напечатать адреса всех ячеек.

Код для чтения · разбираем, набирать не нужно
	count := 0
	for shelf := 1; shelf <= 3; shelf++ {
		for cell := 1; cell <= 4; cell++ {
			fmt.Print(shelf, "-", cell, " ")
			count++
		}
		fmt.Println()
	}
	fmt.Println("Ячеек:", count)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
1-1 1-2 1-3 1-4 
2-1 2-2 2-3 2-4 
3-1 3-2 3-3 3-4 
Ячеек: 12

Внутренний цикл — просто строка тела внешнего. На каждом проходе внешнего он выполняется целиком: cell снова начинается с 1 и доходит до 4. Тело внутреннего цикла выполнилось 3 × 4 = 12 раз, fmt.Println() — три раза, по разу на полку. У переменных разные имена: shelf и cell. Если назвать обе i, внутренняя закроет внешнюю, и в теле внутреннего цикла до номера полки уже не добраться.

break со страницы 3 во вложенных циклах выходит только из того цикла, в котором стоит:

Код для чтения · разбираем, набирать не нужно
	for shelf := 1; shelf <= 3; shelf++ {
		for cell := 1; cell <= 4; cell++ {
			if cell == 3 {
				break
			}
			fmt.Print(shelf, "-", cell, " ")
		}
		fmt.Println("| полка", shelf)
	}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
1-1 1-2 | полка 1
2-1 2-2 | полка 2
3-1 3-2 | полка 3

Внутренний цикл обрывается на третьей ячейке, внешний продолжает полки.

Проверить таблицу трассировки запуском

Отладочная печать работает и в цикле. Копия программы со страницы 4 в scratch с печатью после строки 6 и после каждой строки тела:

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

import "fmt"

func main() {
	sum := 0
	fmt.Println("после строки 6: sum", sum)
	for i := 0; i < 3; i++ {
		fmt.Println("после строки 7: sum", sum, "i", i)
		sum += i
		fmt.Println("после строки 8: sum", sum, "i", i)
	}
	fmt.Println(sum)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
после строки 6: sum 0
после строки 7: sum 0 i 0
после строки 8: sum 0 i 0
после строки 7: sum 0 i 1
после строки 8: sum 1 i 1
после строки 7: sum 1 i 2
после строки 8: sum 3 i 2
3

Сравните с таблицей на странице 4: здесь нет шага 8, последней проверки. Печать «после строки 7» стоит первой строкой тела и выполняется, только когда условие истинно. Ложная проверка отладочной печатью не видна — её дописывают по правилу 12. И шага 9 с прочерком тоже нет: i после цикла не напечатать, компилятор скажет undefined: i.

В подписях — номера строк исходной программы: в копии строки сдвинулись. Поэтому, как и в день 02, опыты — только в копии, а номера в таблице — по исходному файлу.

Ввод с клавиатуры: Ctrl+D и Ctrl+C

Без < и | программа читает строки с клавиатуры. Каждая строка уходит программе по Enter, а конец ввода — Ctrl+D в начале новой строки:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
5
прочитал: 5
12
прочитал: 12
ввод кончился

Строки 5 и 12 набраны руками, Ctrl+D на экране не видно: программа просто получила «ввод кончился» и доработала до конца.

Если запустить go run . и ничего не набирать, программа молчит — это не зависание, она ждёт строку. Прервать ожидание можно Ctrl+C:

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

Разница важна. Ctrl+D — «ввод кончился»: программа выходит из цикла и печатает итог. Ctrl+C — «прекратить»: программа обрывается там, где была, строка после цикла не выполнится.

Лишний scanner.Scan() до цикла

Он остаётся от программы дня 07 (в файле импорты bufio, fmt, os):

Код для чтения · разбираем, набирать не нужно
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	line := 0
	for scanner.Scan() {
		line++
		fmt.Println("строка", line, "текст", scanner.Text())
	}
	fmt.Println("строк:", line)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ printf '12\n7\n3\n' | go run .
строка 1 текст 7
строка 2 текст 3
строк: 2

Первый Scan() прочитал 12, но его Text() никто не взял, а следующий Scan() в заголовке прочитал уже 7. Строка потерялась молча.

Минимум и начальное значение

Попробуйте сейчас: минимум с least := 0 и с least := 1000.

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

1. Наберите в scratch/main.go первую программу про минимум со страницы 7 — ту, где least := 0 и сравнение без номера прохода:

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

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	least := 0
	for scanner.Scan() {
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		if qty < least {
			least = qty
		}
	}
	fmt.Println("Самая мелкая:", least)
}

Запустите её с day.txt и с /dev/null:

▶ Выполните
cd ~/gocourse/day08/scratch
go run . < day.txt
go run . < /dev/null

2. Замените в ней least := 0 на least := 1000 и запустите снова: на day.txt помогло. На каком вводе сломается опять?

Готово, когда: с least := 0 печатается Самая мелкая: 0, с least := 1000 — Самая мелкая: 5, и вы назвали ввод, на котором и 1000 не спасает.

Флаг «нашли»

Ещё один накопитель — ответ «встречалось ли такое». Нужно узнать, есть ли в выгрузке пустые отгрузки — с нулём штук. Сколько их и где, неважно. Программа целиком:

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

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	found := false
	for scanner.Scan() {
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		if qty == 0 {
			found = true
		}
	}
	if found {
		fmt.Println("Есть пустые отгрузки - проверить накладные")
	} else {
		fmt.Println("Пустых отгрузок нет")
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ printf '12\n0\n5\n' | go run .
Есть пустые отгрузки - проверить накладные
stagiaire@lab:~/gocourse/day08/scratch$ go run . < day.txt
Пустых отгрузок нет

Флаг — накопитель типа bool. До цикла false: пока ничего не нашли. В теле — только found = true и никогда обратно: одна пустая отгрузка уже делает ответ «да». После цикла флаг читают в if. Если после находки остальные строки не нужны, в тело можно добавить break — но тогда и другие накопители остановятся на этой строке.

Флаг, которому только присваивают, компилятор не пропустит: found = true записывает в ячейку, но не читает её. Строки из тела main, в scratch с одним import "fmt":

Код для чтения · разбираем, набирать не нужно
	found := false
	for day := 1; day <= 3; day++ {
		if day == 2 {
			found = true
		}
	}
	fmt.Println("готово")
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
# day08/scratch
./main.go:6:2: declared and not used: found

С total += qty такого не бывает: += сначала читает старое значение.

Мини-опыты

1. Цикл по числу. С Go 1.22 можно писать for i := range 3 — это то же, что for i := 0; i < 3; i++. Наберите в scratch/main.go целиком:

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

import "fmt"

func main() {
	for i := range 3 {
		fmt.Println("Коробка", i)
	}
}

Запустите:

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

С какого числа начнётся счёт? Сколько строк будет? В курсе до блока 4 пишем полную форму: в ней видны все три части и граница.

2. Трассировка вложенного цикла. Возьмите программу «полки × ячейки» из раздела «Вложенные циклы» и составьте таблицу для двух полок и двух ячеек. Сколько раз в таблице встретится строка внутреннего for? Сверьте отладочной печатью.

3. Счётчик вместо печати. В программе про рейсы со страницы 3 уберите печать и добавьте счётчик проверок условия: переменную, которая увеличивается перед циклом и в конце тела. Сколько раз проверялось условие и сколько раз выполнилось тело? Почему на единицу разница?