Своя функция: имя для куска работы
С дня 04 вы вызываете чужие функции: strings.ToUpper, strconv.Atoi, math.Round, fmt.Sprintf. Кто-то их написал, вам достался готовый результат. Сегодня вы пишете такую же — свою.
Одно важное отличие от всего, что было до сих пор: функцию нельзя написать внутри main. Она объявляется рядом с ним, на верхнем уровне файла. Поэтому программы сегодня показаны целиком — и набирают их в scratch/main.go целиком, вместо того что там лежит. Когда дальше в разборе меняются одна-две строки, показаны только они, и прямо сказано, какая это строка программы.
Одно и то же дважды
Ситуация. На двух складах лежит товар, коробка вмещает 6 штук. Нужно сказать, сколько коробок уйдёт на каждый склад. Неполная коробка — тоже коробка.
package main
import "fmt"
func main() {
qtyA := 14
boxesA := qtyA / 6
if qtyA%6 != 0 {
boxesA++
}
fmt.Println("Склад A:", boxesA, "кор.")
qtyB := 30
boxesB := qtyB / 6
if qtyB%6 != 0 {
boxesB++
}
fmt.Println("Склад B:", boxesB, "кор.")
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
Склад A: 3 кор.
Склад B: 5 кор.Как это читать. Программа работает и считает правильно. Плохо в ней другое: четыре строки расчёта написаны дважды, слово в слово, отличаются только имена переменных. Появится склад C — они появятся в третий раз.
Это не вопрос красоты. У повтора есть цена, и она измеримая:
- Правка ложится не везде. Завтра выяснится, что неполную коробку считать не надо: исправлять придётся во всех трёх местах, а забыли одно — склад C считается по старым правилам, и никакой ошибки компилятора не будет.
- Читать дольше. Приходится сличать похожие куски и проверять, одинаковые ли они: глаз на третьем уже не различает
boxesBиboxesC. - У куска нет имени. «Четыре строки, которые считают коробки» — это описание, а не имя.
Функция решает всё это сразу: расчёт лежит в одном месте, и у него есть имя.
Как объявляется функция
Та же задача, тот же вывод:
package main
import "fmt"
func boxes(qty int) int {
n := qty / 6
if qty%6 != 0 {
n++
}
return n
}
func main() {
fmt.Println("Склад A:", boxes(14), "кор.")
fmt.Println("Склад B:", boxes(30), "кор.")
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
Склад A: 3 кор.
Склад B: 5 кор.Как это читать. Первая строка объявления — заголовок функции. В нём четыре части:
| Кусок | Как называется | Что значит |
|---|---|---|
func |
ключевое слово | «дальше объявляется функция» |
boxes |
имя | по нему функцию вызывают |
(qty int) |
параметры | что функция получает снаружи и под каким именем это видно внутри |
int после скобок |
тип результата | что функция отдаёт обратно |
{ … } |
тело | что она делает |
main — тоже функция, вы объявляете её с дня 01. У неё нет ни параметров, ни результата, поэтому после main() ничего не написано: func main() { — не заклинание, а обычный заголовок функции с пустыми скобками. И тело функции живёт по тем же правилам, что тело main: переменные, if, циклы, вызовы других функций. Новое здесь одно — return.
return n делает две вещи сразу:
- заканчивает работу функции — строки после него не выполняются;
- отдаёт значение
nтуда, откуда функцию вызвали.
Вызов boxes(14) — это выражение со значением, такое же, как 2 + 2 или qtyA / 6. Его можно напечатать, положить в переменную, сложить с другим числом, сравнить:
package main
import "fmt"
func boxes(qty int) int {
n := qty / 6
if qty%6 != 0 {
n++
}
return n
}
func main() {
a := boxes(14)
b := boxes(30)
fmt.Println("Всего коробок:", a+b)
fmt.Println("На складе A больше:", a > b)
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
Всего коробок: 8
На складе A больше: falseСтрока a := boxes(14) читается справа налево, как любое присваивание из дня 02: сначала вычисляется правая часть — то есть работает функция, — и её результат кладётся в ячейку a.
Попробуйте сейчас: две программы рядом.
Цель: обе программы — с повтором и с функцией — набраны своими руками и печатают одно и то же.
1. Сотрите заготовку в
scratch/main.goи наберите программу из раздела «Одно и то же дважды» — вот она целиком:✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { qtyA := 14 boxesA := qtyA / 6 if qtyA%6 != 0 { boxesA++ } fmt.Println("Склад A:", boxesA, "кор.") qtyB := 30 boxesB := qtyB / 6 if qtyB%6 != 0 { boxesB++ } fmt.Println("Склад B:", boxesB, "кор.") }Запустите:
▶ Выполнитеcd ~/gocourse/day11/scratch go run .2. Теперь наберите вместо неё программу из раздела «Как объявляется функция» и запустите снова. Вывод должен быть тот же.
✎ Наберите в файлscratch/main.gopackage main import "fmt" func boxes(qty int) int { n := qty / 6 if qty%6 != 0 { n++ } return n } func main() { fmt.Println("Склад A:", boxes(14), "кор.") fmt.Println("Склад B:", boxes(30), "кор.") }▶ Выполнитеgo run .3. Допишите третий склад: в первой программе — ещё четыре строки расчёта, во второй — один вызов
boxes(7). Сравните, сколько пришлось набрать в том и в другом случае.Готово, когда: обе программы печатают
Склад A: 3 кор.иСклад B: 5 кор., а со складом C во второй программе добавилась ровно одна строка.
Функция объявлена — вызывать её не обязательно
Переменную, которую объявили и не использовали, компилятор не пропускает (declared and not used — день 02). С функциями правило другое: объявить можно и не вызывать. Программа, в которой boxes объявлена, а main печатает только готово, собирается и работает — ни слова упрёка.
Причина простая: функцию, объявленную в пакете, может вызвать другой файл того же пакета, и компилятор не считает её лишней. Значит, забытая или недописанная функция будет молча лежать в файле сколько угодно. Порядок объявления тоже не важен: boxes можно написать и до main, и после — об этом пятая страница.
Попробуйте сейчас: своя функция с нуля.
Цель: написана функция, которой не было в тексте, и она вызвана из
mainдважды.Вес посылки складывается из веса товара и веса упаковки: упаковка — 120 г.
1. В
scratch/main.goобъявите функцию с заголовкомfunc parcel(goods int) int, которая возвращает вес посылки в граммах. Тело — одна строка сreturn.2. В
mainнапечатайте вес посылки для товара 800 г и для товара 1500 г — двумя вызовами:▶ Выполнитеcd ~/gocourse/day11/scratch go run .Готово, когда: программа печатает два числа — 920 и 1620, — а слово
120встречается в файле ровно один раз.
Что может пойти не так
Функция обещала результат, но не вернула его:
package main
import "fmt"
func boxes(qty int) int {
n := qty / 6
if qty%6 != 0 {
n++
}
}
func main() {
fmt.Println("Склад A:", boxes(14), "кор.")
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:10:1: missing returnСтрока 10 — закрывающая скобка функции. Компилятор говорит: «дошли до конца тела, а return так и не встретился». Тот же случай, когда return спрятан внутри if, — в программе изменена только функция:
func boxes(qty int) int {
if qty > 0 {
return 1
}
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:9:1: missing returnУсловие может оказаться ложным — и тогда возвращать нечего. Компилятор проверяет не «бывает ли так на самом деле», а «может ли выполнение дойти до конца тела». Лечение — return после if или else с return.
Обратная ошибка: функция ничего не обещала, а её результат печатают:
package main
import "fmt"
func report() {
fmt.Println("отчёт готов")
}
func main() {
fmt.Println(report())
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:10:14: report() (no value) used as valuereport печатает сама и ничего не отдаёт — значит, её вызов не выражение и внутри Println ему не место.
И самый тихий случай: функция вернула значение, а его никто не взял.
package main
import "fmt"
func boxes(qty int) int {
return qty / 6
}
func main() {
boxes(14)
fmt.Println("готово")
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
готовоНи ошибки, ни предупреждения — go vet тоже молчит. Строка boxes(14) законна: функция отработала, вернула 2, и это значение выбросили. С len(s) и другими встроенными функциями так нельзя, а с обычными — можно, поэтому забытое присваивание придётся замечать самому. На второй странице вы почините программу именно с этой ошибкой.
| Что видите | Что это значит | Что делать |
|---|---|---|
./main.go:10:1: missing return |
в заголовке обещан тип результата, а до конца тела return может не встретиться |
return в конце тела или в каждой ветке if/else |
./main.go:10:14: report() (no value) used as value |
результат берут у функции, которая его не возвращает | либо дописать тип результата и return, либо не использовать вызов как значение |
./main.go:6:7: syntax error: unexpected name boxes, expected ( |
объявление функции написано внутри main |
объявлять рядом с main, на верхнем уровне файла |
| программа собирается, но считает не то; вызов стоит отдельной строкой | результат вернулся и был выброшен | присвоить результат или напечатать его |
undefined: boxes |
функция объявлена в другом файле, не в этом пакете, или в имени опечатка | сверить имя буква в букву — пятая страница |
Дальше: параметры, аргументы и главное свойство передачи — course next