День 08 — глубже
Всё здесь необязательно. Материалы на английском помечены.
Тур по Go: цикл for (англ.)
- A Tour of Go, Flow control — https://go.dev/tour/flowcontrol/1 Страницы 1–4:
forс тремя частями, без частей, «while» и бесконечный цикл. Код запускается прямо в браузере.
Что говорит спецификация языка (англ.)
- For statements — https://go.dev/ref/spec#For_statements Точное описание трёх форм
for. В разделе про форму с тремя частями сказано, что каждый проход получает свою копию переменной цикла, — это пригодится в блоке про конкурентность. - Break statements и Continue statements — https://go.dev/ref/spec#Break_statements В конце — метки:
breakс меткой выходит сразу из внешнего цикла, а не только из внутреннего. В разделе «Вложенные циклы» ниже обычныйbreakвыходит только из внутреннего; с меткой можно выйти из обоих.
Effective Go: раздел For (англ.)
- https://go.dev/doc/effective_go#for Короткий раздел о том, как циклы пишут опытные программисты на Go. Там же есть
for ... range— его разберём в блоке 4, когда появятся срезы и строки.
Ошибка на единицу (англ.)
- Off-by-one error — https://en.wikipedia.org/wiki/Off-by-one_error История «забора и столбов» и десяток примеров из реальных программ.
Документация к 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 clausei++ внутри вызова — в 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.gopackage 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.gopackage 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/null2. Замените в ней
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.gopackage main
import "fmt"
func main() {
for i := range 3 {
fmt.Println("Коробка", i)
}
}Запустите:
cd ~/gocourse/day08/scratch
go run .С какого числа начнётся счёт? Сколько строк будет? В курсе до блока 4 пишем полную форму: в ней видны все три части и граница.
2. Трассировка вложенного цикла. Возьмите программу «полки × ячейки» из раздела «Вложенные циклы» и составьте таблицу для двух полок и двух ячеек. Сколько раз в таблице встретится строка внутреннего for? Сверьте отладочной печатью.
3. Счётчик вместо печати. В программе про рейсы со страницы 3 уберите печать и добавьте счётчик проверок условия: переменную, которая увеличивается перед циклом и в конце тела. Сколько раз проверялось условие и сколько раз выполнилось тело? Почему на единицу разница?