Данные через аргументы, а не через общие переменные
Функцию вынесли — и сразу встаёт вопрос: откуда она возьмёт числа. Ответов два, и они не равноценны. Разберём оба и договоримся, каким пользуемся в курсе и почему.
Способ первый: общая переменная
В 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.gopackage 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.gopackage 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).
Как не попадаться:
- Единица — в имени.
priceKop,priceRub,weightG,weightKg. Имяpriceне говорит ничего, и именно на таких именах путаются. - Одна единица на всю программу. В курсе деньги везде в копейках (день 03), в рубли переводят только в момент печати.
- Проверяйте порядок величины. Заказ на семь штук по сорок пять рублей никак не может стоить три рубля.
Опечатка в имени при вызове
Единственная ошибка компилятора, которая в этой теме встречается чаще всего:
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: boxesFroundefined значит «такого имени в пакете нет». Компилятор не ищет похожие имена и не подсказывает, что вы имели в виду: он просто не знает имени 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