Написать функцию по чужому описанию
До сих пор функции придумывали вы сами. На работе чаще приходит описание: в тикете, в комментарии коллеги, в документации к сервису. Написано, как функция должна называться, что принимает и что возвращает, — а тело пишете вы. Сегодняшнее задание report устроено ровно так.
Описание функции — это договор
Полное описание отвечает на четыре вопроса.
| Вопрос | Где ответ | Пример |
|---|---|---|
| Как называется и что принимает | заголовок | func pallets(boxes, perPallet int) int |
| В чём измеряются аргументы | текст рядом | boxes — коробок, perPallet — коробок на поддон |
| Что возвращает | текст и тип | сколько поддонов нужно |
| Что на границах | текст | неполный поддон — тоже поддон; при нуле коробок — 0 |
Четвёртый вопрос — самый важный и самый пропускаемый. Обычные случаи пишут все, а расходятся на границах: ровно на пороге, при нуле, при пустых данных. Если про границу в описании ничего не сказано — это дырка в договоре, и на работе её выясняют до того, как писать код.
Разбор: поддоны
Описание. func pallets(boxes, perPallet int) int — сколько поддонов нужно на boxes коробок, если на один поддон входит perPallet коробок. Неполный поддон тоже занимает место. Если коробок 0, поддонов 0.
Шаг 1. Примеры на бумаге — до кода. Четыре строки, из них три пограничные:
| boxes | perPallet | Поддонов | Почему |
|---|---|---|---|
| 0 | 8 | 0 | коробок нет |
| 1 | 8 | 1 | одна коробка занимает целый поддон |
| 8 | 8 | 1 | вошло ровно, лишнего поддона нет |
| 9 | 8 | 2 | одна лишняя коробка требует второго |
Шаг 2. Тело. Это знакомое «разделить и округлить вверх» из дня 05:
package main
import "fmt"
func pallets(boxes, perPallet int) int {
return (boxes + perPallet - 1) / perPallet
}
func main() {
fmt.Println(pallets(0, 8))
fmt.Println(pallets(1, 8))
fmt.Println(pallets(8, 8))
fmt.Println(pallets(9, 8))
}Шаг 3. Сверка с таблицей.
stagiaire@lab:~/gocourse/day13/scratch$ go run .
0
1
1
2Четыре числа совпали с четырьмя строками таблицы. Обратите внимание, что main здесь — не программа, а способ проверить функцию: четыре вызова с известными ответами. Завтра, в день 14, эту же работу будет делать тест, и делать её будет каждый раз сам.
Если бы в теле стояло boxes/perPallet + 1, первые две строки таблицы разошлись бы: для 0 получилось бы 1, для 8 — 2. Пограничные примеры и нужны, чтобы поймать это до сдачи.
Как писать по описанию: порядок
- Перепишите заголовок дословно. Имя, порядок параметров, типы — как в описании. Поменяли порядок параметров местами — вызовы в чужом коде сломаются молча: типы-то сойдутся.
- Выпишите примеры, включая границы. Три-четыре строки таблицы «дано — должно получиться». Ответы считаете вы, а не программа.
- Напишите тело. Одно дело, без печати и без чтения ввода, если в описании не сказано иначе.
- Проверьте на своих примерах — вызовами из
mainили сверкой с посчитанным на бумаге. gofmt,go vet, коммит.
Функция по описанию ничего не печатает не из вредности. Печать — забота того, кто её вызвал: он решает, куда и в каком виде показать число. Функция, которая печатает сама, годится ровно для одного места в программе.
Чего в описании нет — о том и спрашивают
Описание в тикете почти никогда не бывает полным. Дырки видны, если задать ему четыре вопроса из таблицы выше. Вот типичное описание из рабочей переписки и вопросы к нему:
«Нужна функция, которая считает скидку по сумме заказа. Если заказ большой, скидка десять процентов».
| Вопрос к описанию | Почему он важен |
|---|---|
| «большой» — это сколько и в чём: в рублях, копейках, штуках? | без числа и единицы функцию не написать |
| граница входит? заказ ровно на пороге — скидка есть? | на этом расходятся чаще всего |
| скидка возвращается или сразу вычитается из суммы? | это две разные функции с разными результатами |
| дробная копейка отбрасывается или округляется? | 86.415 копейки — это 86 или 87 |
| отрицательная сумма бывает? | если да, нужно поведение; если нет, так и записать |
В курсе все ответы уже вписаны в TASK.txt — читайте условие целиком, включая раздел про границы. На работе за ответами идут к тому, кто поставил задачу, до того, как написан код: переписать функцию дешевле, чем объяснять, почему на границе получилось не то.
Ответы, которые вы получили, полезно записать прямо над функцией комментарием — тогда следующий читающий не пойдёт спрашивать заново:
// pallets возвращает число поддонов на boxes коробок: неполный поддон
// тоже занимает место целиком. При нуле коробок возвращает 0.
func pallets(boxes, perPallet int) int {
return (boxes + perPallet - 1) / perPallet
}Такой комментарий в Go пишут над объявлением и начинают с имени функции — так его показывает всплывающая подсказка редактора и команда go doc -u:
stagiaire@lab:~/gocourse/day13/scratch$ go doc -u pallets
func pallets(boxes, perPallet int) int
pallets возвращает число поддонов на boxes коробок: неполный поддон тоже
занимает место целиком. При нуле коробок возвращает 0.Ключ -u здесь обязателен. Без него go doc показывает только функции, имя которых начинается с большой буквы, — те, которыми пакет делится с другими пакетами (про большую и маленькую букву в имени — в блоке 5). Все свои функции в курсе начинаются с маленькой, поэтому go doc pallets без ключа отвечает doc: no symbol pallets in package …, а go doc -u показывает и их.
Ошибки вокруг функций
Все сообщения ниже — с этого стенда, Go 1.27.1.
Функции нет. Задание report начинается именно с этого:
# day13/report
./main.go:17:7: undefined: discountundefined — имя в пакете не объявлено. Либо функция ещё не написана, либо опечатка в имени, либо она объявлена в другом каталоге: файлы одного пакета лежат в одной папке, и report/main.go не видит функций из split/main.go.
Не на всяком пути есть return:
func rubles(kopecks int) string {
fmt.Sprintf("%d.%02d", kopecks/100, kopecks%100)
}# day13/scratch
./main.go:7:1: missing returnКолонка указывает на закрывающую скобку функции: компилятор дошёл до её конца и не встретил return. Здесь вызов Sprintf есть, а результат никуда не отдан.
Число аргументов не сошлось. Две строки на каждое сообщение — have показывает, что передали, want — что объявлено:
# day13/scratch
./main.go:10:23: not enough arguments in call to packCost
have (number)
want (int, int)
./main.go:11:30: too many arguments in call to packCost
have (number, number, number)
want (int, int)Две функции с одним именем. В Go нельзя объявить две функции с одинаковым именем, даже если у них разное число параметров:
# day13/scratch
./main.go:9:6: boxes redeclared in this block
./main.go:5:6: other declaration of boxesВторая строка показывает, где объявление было первым. Лечение — разные имена: boxesFor и boxesForPallet.
Функция объявлена внутри main. Так бывает, когда кусок кода оборачивают в func прямо на месте:
# day13/scratch
./main.go:6:7: syntax error: unexpected name boxesFor, expected (Свои функции объявляют на верхнем уровне файла — рядом с main, до или после него, но не внутри.
Попробуйте сейчас: дописать функцию discount.
Цель: программа
reportсобирается и печатает три строки, а функцияdiscountведёт себя так, как написано в условии.1. Прочитайте условие и посмотрите, что сейчас говорит компилятор:
▶ Выполнитеcd ~/gocourse/day13/report cat TASK.txt go run .2. Выпишите на бумаге четыре примера — обычный случай и три границы из раздела условия «ГРАНИЦЫ»: сумма ровно равна порогу, процент 0, сумма ниже порога.
3. Напишите функцию — в
main.goрядом сmainили в отдельном файлеdiscount.goв той же папке. Проверьте:▶ Выполнитеgo run . gofmt -l .4. Закоммитьте:
▶ Выполнитеcd ~/gocourse/day13 git add . git commit -m "report: написал discount"Готово, когда: программа печатает заказ, скидку и сумму к оплате, и эти три числа совпали с посчитанными на бумаге — пункт
report. Проверка прогоняет вашу функцию на своих числах, в том числе на границах.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
./main.go:17:7: undefined: discount |
функции нет в пакете | написать её в той же папке, package main |
./main.go:7:1: missing return |
путь без return |
вернуть значение на каждом пути, в том числе из if |
пункт report красный, а программа печатает правдоподобно |
на границе поведение другое, чем в условии | перечитать условие: «не меньше» — это порог включительно |
| скидка на копейку больше ожидаемой | округление вверх вместо отбрасывания | целое деление уже отбрасывает дробь, math.Ceil не нужен |
| функция печатает и возвращает | дел два | печать оставить в main |
syntax error: unexpected name discount, expected ( |
func discount написана внутри main |
вынести объявление на верхний уровень файла |
функция написана в split, а нужна в report |
пакет — это папка | перенести файл в папку задания |
Дальше: когда разбиение на функции вредит, и словарик — course next