День 08 · Циклы: for, границы, накопители · страница 6 из 7

Накопители: сумма, количество, среднее

Цикл видит одну строку за раз, а итог дня нужен после последней, когда строк уже нет. Значит, по ходу цикла что-то должно накапливаться.


Накопитель живёт до цикла

Ситуация. В scratch/day.txt отгрузки за день, по числу в строке; нужны число отгрузок, сумма и среднее.

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	total := 0
	count := 0
	for scanner.Scan() {
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		total += qty
		count++
	}
	fmt.Println("Отгрузок:", count)
	fmt.Println("Всего, шт.:", total)
	fmt.Printf("В среднем: %.2f\n", float64(total)/float64(count))
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run . < day.txt
Отгрузок: 4
Всего, шт.: 97
В среднем: 24.25

Как это читать. Накопитель — переменная с тремя местами:

Где Сумма total Количество count
до цикла — начальное значение total := 0 count := 0
в теле — на каждом проходе total += qty count++
после цикла — результат печать печать

Начальное значение — то, что должно получиться, если строк нет: сумма 0, количество 0. Среднее — не накопитель: его считают один раз после цикла.

float64(total)/float64(count) — дробное деление из дня 03. Поделите целые — и дробная часть пропадёт до преобразования (программа с одним import "fmt"):

Код для чтения · разбираем, набирать не нужно
	total := 57
	count := 4
	fmt.Println(total / count)
	fmt.Println(float64(total / count))
	fmt.Printf("%.2f\n", float64(total)/float64(count))
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run .
14
14
14.25

Попробуйте сейчас: итог дня на двух файлах.

Цель: программа итога работает на day.txt, и вы знаете, чего ей не хватает для пустых строк.

1. Наберите её в scratch/main.go:

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	total := 0
	count := 0
	for scanner.Scan() {
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		total += qty
		count++
	}
	fmt.Println("Отгрузок:", count)
	fmt.Println("Всего, шт.:", total)
	fmt.Printf("В среднем: %.2f\n", float64(total)/float64(count))
}

Запустите с day.txt:

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

2. Потом с lines.txt — там вторая строка пустая:

▶ Выполните
go run . < lines.txt

Готово, когда: с day.txt вывод такой же, как выше, а про lines.txt вы можете сказать, что случилось и какой строки не хватает.


Накопитель внутри тела

Ситуация. Объявлять переменную рядом с тем местом, где она меняется, — частая привычка; здесь она ломает счёт.

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		total := 0
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		total += qty
		fmt.Println("Всего, шт.:", total)
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run . < day.txt
Всего, шт.: 12
Всего, шт.: 40
Всего, шт.: 5
Всего, шт.: 40

Как это читать. total := 0 стоит в теле и выполняется на каждом проходе: новая ячейка с нулём, к ней одна отгрузка. Программа при этом собирается: total объявлена и прочитана в total += qty, придраться нечему. Перенесите печать после цикла — компилятор отзовётся, но не о причине, а о следствии:

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		total := 0
		qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
		if err != nil {
			fmt.Fprintln(os.Stderr, "Не число:", scanner.Text())
			os.Exit(1)
		}
		total += qty
	}
	fmt.Println("Всего, шт.:", total)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run . < day.txt
# day08/scratch
./main.go:22:36: undefined: total

Правило то же, что у переменной цикла: объявленное в теле живёт до его }. Лечение одно — объявлять накопитель до цикла.


Попробуйте сейчас: починить накопитель в fix2.

Цель: fix2 печатает верный нарастающий итог; правка — одна-две строки, больше двух проверка не примет.

1. Запустите:

▶ Выполните
cd ~/gocourse/day08/fix2
go run . < example.txt

2. Сравните с комментарием в начале файла и протрассируйте в голове два первых прохода: где создаётся total и что в ней лежит перед +=?

Готово, когда: go run . < example.txt печатает ровно те три строки, что в комментарии, а diff с оригиналом — одну-две изменённые строки (пункт fix2).


Пустой ввод: делить не на что

Та же программа на пустом вводе (/dev/null — файл из нуля строк).

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run . < /dev/null
Отгрузок: 0
Всего, шт.: 0
В среднем: NaN

NaN — not a number, «не число»: так %.2f печатает дробный ноль, делённый на ноль. А с целым делением total/count будет паника:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day08/scratch$ go run . < /dev/null
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
	/home/stagiaire/gocourse/day08/scratch/main.go:24 +0x215
exit status 2

Путь в четвёртой строке — ваш домашний каталог, а число после + зависит от сборки: у вас будут свои. Важны первая строка и номер строки в main.go. Пустой ввод проверяют после цикла, до деления: if count == 0 { … } else { … }.


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

Что видите Что это значит Что делать
«итог» равен последнему значению накопитель объявлен в теле и обнуляется каждый проход объявлять до цикла
undefined: total после цикла он объявлен в теле, дальше его нет объявление — до цикла
В среднем: NaN или panic: runtime error: integer divide by zero деление при count = 0, дробное или целое проверить count == 0 до деления
среднее без дробной части: 14 вместо 14.25 сначала целое деление, потом float64 преобразовать оба числа до деления
отгрузок на единицу больше, чем чисел count++ до continue: считает пустые строки счётчик — после всех проверок

Попробуйте сейчас: задание total — итог дня.

Цель: total/main.go печатает итог дня по TASK.txt.

1. Прочитайте условие:

▶ Выполните
cd ~/gocourse/day08/total
cat TASK.txt

2. Напишите программу в main.go. Счётчиков здесь два: номер строки во вводе (для сообщения об ошибке) и число отгрузок (для итога), и растут они в разных местах тела — решите где, прежде чем писать.

3. Проверить себя на примерах из условия:

▶ Выполните
go run . < example1.txt
go run . < example2.txt
go run . < /dev/null

Сообщение об ошибке идёт в поток ошибок, и программа выходит с кодом 1: проверка смотрит оба потока по отдельности.

Готово, когда: на example1.txt и example2.txt вывод совпадает с примерами в TASK.txt, на /dev/null — одна строка Отгрузок не было, а gofmt -l . ничего не печатает — пункты total и code_fmt.


Дальше: максимум и минимум — course next