Параметры и аргументы: внутрь уходит копия
Функция без параметров считала бы одно и то же число. Всю пользу даёт то, что снаружи ей передают данные: boxes(14) и boxes(30) — одна и та же функция и два разных ответа. Сегодняшняя страница — про то, как данные попадают внутрь и что с ними там происходит.
Два имени для одного и того же
Ситуация. Цена за штуку перестала быть постоянной: у разных товаров она разная. Значит, функции нужно передавать два числа.
package main
import "fmt"
func cost(qty int, price int) int {
return qty * price
}
func main() {
fmt.Println("Заказ 3 шт.:", cost(3, 4500), "коп.")
fmt.Println("Заказ 12 шт.:", cost(12, 4500), "коп.")
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
Заказ 3 шт.: 13500 коп.
Заказ 12 шт.: 54000 коп.Как это читать. У одного и того же числа здесь два имени, и важно их не путать:
| Слово | Где живёт | Пример выше |
|---|---|---|
| параметр | в заголовке функции; это имя, под которым значение видно внутри | qty, price |
| аргумент | в вызове; это значение, которое передают снаружи | 3 и 4500 |
Параметры перечисляются через запятую, у каждого свой тип. Подряд идущие одинаковые типы можно не повторять — func cost(qty, price int) int значит ровно то же самое, — но в курсе пишем полную форму.
Аргументы попадают в параметры строго по порядку. Первый аргумент — в первый параметр, второй — во второй; имена переменных в main на это не влияют. Можно передать cost(n, p), cost(3, price) или cost(qty*2, 4500) — считается только позиция. Это источник тихой ошибки, о которой будет пятая страница.
Параметр — обычная переменная функции, просто созданная не :=, а самим вызовом: её видно в любом месте тела, ей можно присваивать.
Сколько аргументов и какого типа
Компилятор сверяет вызов с заголовком по числу аргументов и по типам. Дальше — та же программа, в которой меняется только main. Аргументов меньше, чем параметров: в main остался один вызов, строка 10, и в нём пропал второй аргумент.
fmt.Println(cost(3))stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:10:19: not enough arguments in call to cost
have (number)
want (int, int)Три строки: что не так, что передали (have) и что нужно (want). number вместо int — потому что 3 в исходнике ещё не число определённого типа, а просто число, как константы из дня 03.
Аргументов больше — та же программа, изменена только строка 10:
fmt.Println(cost(3, 4500, 2))stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:10:28: too many arguments in call to cost
have (number, number, number)
want (int, int)Тип не тот — в main добавлена строка со строкой в кавычках:
size := "3"
fmt.Println(cost(size, 4500))stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:11:19: cannot use size (variable of type string) as int value in argument to costСообщение длинное, но читается по частям: «нельзя использовать size (переменная типа string) как значение int в аргументе функции cost». Строка, в которой лежит "3", числом не станет сама — нужен strconv.Atoi из дня 07.
Ещё одно правило, о котором стоит знать заранее: параметр, который функция не использует, — не ошибка.
func label(qty int, note string) string {
return "коробка"
}Вызов fmt.Println(label(3, "срочно")) собирается и печатает коробка: ни qty, ни note в теле не читаются, и компилятор молчит. declared and not used для параметров не бывает никогда — ни для одного, ни для всех сразу. Правило «объявил — используй» касается только переменных, созданных внутри тела: строка rest := qty % 10, если rest нигде не читают, даст знакомое с дня 02 declared and not used: rest.
Попробуйте сейчас: три ошибки вызова.
Цель: увидеть своими глазами все три сообщения о неверном вызове и записать их в словарик.
1. Наберите в
scratch/main.goпрограмму с функциейcostиз раздела «Два имени для одного и того же» — вот она целиком:✎ Наберите в файлscratch/main.gopackage main import "fmt" func cost(qty int, price int) int { return qty * price } func main() { fmt.Println("Заказ 3 шт.:", cost(3, 4500), "коп.") fmt.Println("Заказ 12 шт.:", cost(12, 4500), "коп.") }Запустите — она должна работать:
▶ Выполнитеcd ~/gocourse/day11/scratch go run .2. Теперь по очереди сломайте вызов и каждый раз запускайте
go run .: уберите второй аргумент; верните и добавьте третий; верните и передайте первым аргументом строку в кавычках. Каждое сообщение — строкой в~/errors.md: текст ошибки, тире, что значило.Готово, когда: вы получили все три сообщения и в
~/errors.mdпоявились строки проnot enough arguments,too many argumentsиcannot use … as int value in argument— они идут в пунктerrors.
Внутрь уходит копия
Ситуация. Кажется естественным: раз параметр — это переменная, то, меняя её в функции, мы меняем то, что передали. Проверим.
package main
import "fmt"
func addOne(n int) {
n = n + 1
fmt.Println("внутри функции n =", n)
}
func main() {
qty := 5
addOne(qty)
fmt.Println("в main qty =", qty)
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
внутри функции n = 6
в main qty = 5Как это читать. В момент вызова Go берёт значение аргумента и кладёт его копию в новую ячейку — параметр n. Дальше это два разных места в памяти: qty в main и n в addOne. Присваивание n = n + 1 меняет только вторую ячейку, а когда функция заканчивается, её ячейки перестают существовать — от изменения не остаётся ничего. Это ровно то же копирование, что при обычном присваивании в дне 02: b := a копирует значение, и потом b = b + 1 не трогает a.
Правило коротко: менять параметр внутри функции бесполезно, если результат не возвращён. Единственный способ отдать что-то наружу — return, и единственный способ этим воспользоваться — присвоить возвращённое значение или сразу его использовать. В той же программе у функции появился тип результата и return, а вызов в main стал присваиванием — qty = addOne(qty):
func addOne(n int) int {
n = n + 1
return n
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
в main qty = 6В строке qty = addOne(qty) справа qty читается — её значение копируется в параметр, — а слева в ту же qty записывается результат. Порядок тот же, что у x = x + 1 из дня 02.
Про срезы, map и указатели, где эта картина сложнее, речь пойдёт в блоках 4 и 5. Пока параметры — числа, строки и bool, и правило работает без исключений.
Попробуйте сейчас: что напечатает программа.
Цель: предсказать вывод программы, где функция меняет параметр, и сдать ответ.
1. Сначала не запуская — что напечатает эта программа?
Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func discount(price int) int { price = price - 500 return price } func main() { price := 4500 discount(price) fmt.Println("Цена:", price) }2. Сдайте число, которое напечатается после слова
Цена::▶ Выполните · выделенное замените своимcourse answer day11.q1 ЧИСЛОВместо
ЧИСЛО— ваше предсказание.3. Проверьте запуском — наберите эту программу в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage main import "fmt" func discount(price int) int { price = price - 500 return price } func main() { price := 4500 discount(price) fmt.Println("Цена:", price) }▶ Выполнитеcd ~/gocourse/day11/scratch go run .Готово, когда: пункт
q1вcourse check 11зелёный.
Попробуйте сейчас: два вызова, одно присваивание.
Цель: предсказать вывод программы, где один вызов присвоен, а другой нет.
1. Не запуская — что напечатает эта программа? Разберите отдельно строку
weight = addPack(weight)и отдельноaddPack(weight).Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func addPack(weight int) int { weight = weight + 200 return weight } func main() { weight := 1000 weight = addPack(weight) addPack(weight) fmt.Println("Вес:", weight) }2. Сдайте число, которое напечатается после слова
Вес::▶ Выполните · выделенное замените своимcourse answer day11.q2 ЧИСЛО3. Проверьте запуском — наберите эту программу в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage main import "fmt" func addPack(weight int) int { weight = weight + 200 return weight } func main() { weight := 1000 weight = addPack(weight) addPack(weight) fmt.Println("Вес:", weight) }▶ Выполнитеcd ~/gocourse/day11/scratch go run .Готово, когда: пункт
q2вcourse check 11зелёный.
Попробуйте сейчас: починить fix1.
Цель:
fix1печатает вес партии вместе с упаковкой; правка — одна-две строки, больше двух проверка не примет.1. Запустите как есть и прочитайте комментарий в начале файла:
▶ Выполнитеcd ~/gocourse/day11/fix1 go run .2. Функция
addPackсчитает верно — убедитесь в этом, прочитав её. Значит, дело в том, что происходит с её результатом. Числа вmainне меняйте, правьте одну-две строки. После правки запустите снова и сравните с оригиналом:▶ Выполнитеgo run . diff ~/.course/orig/day11/fix1/main.go main.goГотово, когда: напечатанный вес больше исходного веса товара ровно на вес упаковки всех коробок, а
diffпоказывает одну-две изменённые строки — пунктыfix1иfixsmall.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
not enough arguments in call to cost и две строки have/want |
аргументов меньше, чем параметров | сверить вызов с заголовком: сколько параметров, столько аргументов |
too many arguments in call to cost |
аргументов больше | лишний аргумент убрать |
cannot use size (variable of type string) as int value in argument to cost |
тип аргумента не совпал с типом параметра | преобразовать: strconv.Atoi для строки, float64(x) для числа |
declared and not used: rest внутри функции |
переменная объявлена в теле и не читается | использовать или удалить; на параметры это правило не распространяется |
| функция меняет параметр, а снаружи ничего не меняется | внутрь ушла копия | вернуть значение через return и присвоить его при вызове |
| вызов стоит отдельной строкой, программа печатает старое значение | результат вернулся и был выброшен | x = f(x) вместо f(x) |
cannot use qty (variable of type int) as string value in argument to label |
перепутаны местами аргументы разных типов | аргументы сопоставляются по порядку, а не по имени |
Дальше: функция, которая ничего не возвращает, и выход из неё досрочно — course next