День 13 · Разбить задачу на функции · страница 4 из 7

Данные через аргументы, а не через общие переменные

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


Способ первый: общая переменная

В Go переменную можно объявить не внутри функции, а рядом с функциями, на самом верхнем уровне файла. Такую переменную называют пакетной: она видна всем функциям пакета, и все они могут её менять.

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

import "fmt"

var stock = 40

func take(n int) int {
	stock = stock - n
	return stock
}

func main() {
	fmt.Println(take(10))
	fmt.Println(take(5))
	fmt.Println(stock)
}

Попробуйте сейчас: общая переменная на всех.

Цель: предсказать три строки вывода программы с пакетной переменной и сдать ответ.

1. Не запуская, выпишите на бумаге, что лежит в stock после каждого вызова. Разбор — в разделе «Что увидите» сразу под этим блоком: читайте его после того, как отправите ответ.

2. Сдайте три числа через пробел — в том порядке, в каком они напечатаются:

▶ Выполните · выделенное замените своим
course answer day13.q3 ЧИСЛО ЧИСЛО ЧИСЛО

3. Проверьте запуском — наберите ту же программу в scratch/main.go:

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

import "fmt"

var stock = 40

func take(n int) int {
	stock = stock - n
	return stock
}

func main() {
	fmt.Println(take(10))
	fmt.Println(take(5))
	fmt.Println(stock)
}
▶ Выполните
cd ~/gocourse/day13/scratch
go run .

Готово, когда: пункт q3 в course check 13 зелёный, и вы можете объяснить, почему третья строка не 40.


Что увидите.

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

Как это читать. var stock = 40 стоит вне всяких функций, поэтому := здесь нельзя — только var. Ячейка одна на всю программу. take не получает остаток аргументом и не имеет его в параметрах: она лезет в общую ячейку, уменьшает её и возвращает новое значение. После двух вызовов в stock лежит 25 — и main видит это изменение, хотя сам ничего не присваивал.

Именно это и есть главная неприятность. Посмотрите на строку fmt.Println(stock) в main: чтобы сказать, что она напечатает, мало прочитать main — надо ещё прочитать все функции, которые были вызваны до неё, и проверить, не трогает ли какая-нибудь из них stock. В программе из десяти функций это десять проверок на каждую строку.


Способ второй: аргументы и возврат

Та же задача, но take получает остаток аргументом и возвращает новый, ничего не меняя снаружи:

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

import "fmt"

func take(stock, n int) int {
	return stock - n
}

func main() {
	stock := 40
	stock = take(stock, 10)
	fmt.Println(stock)
	stock = take(stock, 5)
	fmt.Println(stock)
	fmt.Println(stock)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day13/scratch$ go run .
30
25
25

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

Вопрос Пакетная переменная Аргументы
что вернёт take(5) зависит от того, что было раньше всегда stock - 5 от переданного
кто меняет stock любая функция пакета только строка с = в main
можно ли проверить take отдельно нет: результат зависит от истории да: дал числа — получил число
что будет, если переставить вызовы местами другой ответ тот же ответ для тех же аргументов

Последняя строка — то, за что придётся платить весь оставшийся курс. Функция, которая зависит только от своих аргументов, проверяется одним сравнением: посчитали на бумаге, сравнили. Про такую функцию можно написать тест — этим займёмся завтра, в день 14. Функцию, которая лезет в общую ячейку, придётся проверять целой программой и в нужном порядке.

Поэтому в курсе пакетных переменных нет. Не потому, что они запрещены в Go, — они есть в любом настоящем сервисе (обычно для настроек, которые задаются один раз при старте и дальше не меняются). Их нет потому, что данные, которые меняются по ходу работы, передают аргументами и возвращают результатом. Увидите в чужом коде меняющуюся пакетную переменную — знайте, что найти в такой программе ошибку на порядок труднее.

Ещё одна мелочь, из-за которой пакетная переменная опасна: неиспользуемую локальную переменную компилятор не пропускает (declared and not used, день 02), а неиспользуемую пакетную — пропускает молча. Забытая наверху файла var stock = 40 не вызовет ни одного сообщения.


Единицы измерения — часть аргумента

Функция получает числа. Число не помнит, в чём оно: в штуках, коробках, рублях или копейках. Помнить обязаны вы.

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

import "fmt"

func totalKopecks(priceKop, qty int) int {
	return priceKop * qty
}

func rubles(kopecks int) string {
	return fmt.Sprintf("%d.%02d", kopecks/100, kopecks%100)
}

func main() {
	priceRub := 45
	qty := 7
	total := totalKopecks(priceRub, qty)
	fmt.Println("Штук:", qty)
	fmt.Println("Итого:", rubles(total), "руб.")
}

Функция totalKopecks ждёт цену в копейках — это написано в имени параметра. В main цена лежит в рублях, тоже написано в имени. При вызове рубли ушли туда, где ждали копейки.

Компилятор молчит: оба числа — int, типы сошлись. Молчит и go vet. Программа собирается и печатает неправдоподобный, но правдоподобно выглядящий итог.

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

Цель: предсказать неверный итог программы выше и сдать его.

1. Не запуская, посчитайте на бумаге, что напечатает вторая строка этой программы: цена 45, количество 7, а rubles делит на 100 и печатает две цифры после точки. Вывод показан сразу под этим блоком — смотрите туда после того, как отправите ответ.

2. Сдайте число — только число, без слов:

▶ Выполните · выделенное замените своим
course answer day13.q4 ЧИСЛО.ЧИСЛО

3. Проверьте запуском — наберите ту же программу в scratch/main.go:

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

import "fmt"

func totalKopecks(priceKop, qty int) int {
	return priceKop * qty
}

func rubles(kopecks int) string {
	return fmt.Sprintf("%d.%02d", kopecks/100, kopecks%100)
}

func main() {
	priceRub := 45
	qty := 7
	total := totalKopecks(priceRub, qty)
	fmt.Println("Штук:", qty)
	fmt.Println("Итого:", rubles(total), "руб.")
}
▶ Выполните
cd ~/gocourse/day13/scratch
go run .

4. Потом поправьте вызов на totalKopecks(priceRub*100, qty) и запустите ещё раз — во второй раз должно получиться 315.00.

Готово, когда: пункт q4 в course check 13 зелёный, а после правки программа печатает Итого: 315.00 руб.


Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day13/scratch$ go run .
Штук: 7
Итого: 3.15 руб.

Правка — в одном выражении вызова: totalKopecks(priceRub*100, qty).

Как не попадаться:


Опечатка в имени при вызове

Единственная ошибка компилятора, которая в этой теме встречается чаще всего:

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

import "fmt"

func boxesFor(items int) int {
	return (items + 11) / 12
}

func main() {
	fmt.Println(boxesFro(50))
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day13/scratch$ go run .
# day13/scratch
./main.go:10:14: undefined: boxesFro

undefined значит «такого имени в пакете нет». Компилятор не ищет похожие имена и не подсказывает, что вы имели в виду: он просто не знает имени boxesFro. Три причины, по которым это бывает: опечатка в вызове, опечатка в объявлении, функцию ещё не написали.


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

Что видите Что это значит Что делать
./main.go:10:14: undefined: boxesFro имени нет в пакете: опечатка или функция не написана сверить с объявлением; в редакторе F12 на имени никуда не прыгает
итог меньше в сто раз рубли переданы туда, где ждут копейки единицу — в имя переменной и параметра, перевод — в одном месте
функция возвращает разное на одних и тех же аргументах она читает или меняет пакетную переменную передать значение аргументом, вернуть результатом
./main.go:10:23: not enough arguments in call to packCost и две строки have/want аргументов меньше, чем параметров сверить вызов с заголовком функции
cannot use "50" (untyped string constant) as int value in argument to boxesFor аргумент не того типа передать число, а не строку; день 03 про преобразования
переменная наверху файла объявлена и нигде не нужна пакетная переменная не даёт declared and not used удалить руками, компилятор об этом не скажет

Попробуйте сейчас: починить единицы в fixcall.

Цель: fixcall печатает правдоподобный итог; правка — одна-две строки.

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

▶ Выполните
cd ~/gocourse/day13/fixcall
cat main.go
go run .

2. Посчитайте на бумаге, сколько должно стоить qty штук по цене из priceRub плюс доставка, и сравните с тем, что напечатано. Найдите вызов, в котором единицы разъехались. Объявления и имена не меняйте — меняется одно выражение.

3. После правки запустите снова и сверьте с посчитанным на бумаге.

Готово, когда: go run . печатает три строки, итог совпал с бумагой — пункты fixcall и fixsmall.


Дальше: как разбирать готовую программу и ничего не сломать — course next