День 11 · Своя функция: объявление, параметры, возврат · страница 5 из 7

Имена, порядок объявления и порядок аргументов

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


Глагол для действия, существительное для значения

Ситуация. Одна и та же функция, три разных имени. Какое из них скажет читателю больше?

Образец · набирать не нужно
func f(qty int) int            — ни о чём
func calc(qty int) int         — «считает» что-то
func boxes(qty int) int        — вернёт число коробок

Правило, которым пользуются в Go:

Что делает функция Как называть Примеры
возвращает значение существительным — тем, что вернёт boxes, cost, weight, total
проверяет и отвечает «да/нет» с is, has, can isEmpty, hasDiscount
что-то делает, ничего не возвращает глаголом printRow, checkOrder, saveReport

Плохие имена узнаются по признакам: f, f2, tmp, data, calc, process, doWork. Все они означают «здесь что-то происходит» — то есть ничего. Имя из одной буквы годится для параметра короткой функции (func double(n int) int), но не для самой функции.

Второй признак хорошего имени: вызов читается как фраза. fmt.Println("Склад A:", boxes(14), "кор.") читается вслух, а calc(14) приходится расшифровывать.

Имена в Go пишут вместе, без подчёркиваний, каждое следующее слово с большой буквы: printRow, isEmpty, costWithDiscount. Это не требование компилятора, а соглашение, и gofmt его не меняет.

Про первую букву. Свои функции сегодня называем с маленькой буквы. Заглавная первая буква в Go — не про красоту: она делает имя видимым из других пакетов. Пока пакет у нас один (package main), разницы нет, и программа с func Boxes(...) соберётся и заработает. Но привычка пригодится в блоке 5, где пакетов станет несколько.


Порядок объявления в файле не важен

Ситуация. Внутри main переменную нельзя использовать до того, как она объявлена: строки выполняются сверху вниз. Кажется, что с функциями так же.

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

import "fmt"

func main() {
	fmt.Println("Коробок:", boxes(12))
}

func boxes(qty int) int {
	return qty / 6
}

Что увидите.

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

Как это читать. Работает. Объявления на верхнем уровне файла компилятор собирает все сразу, до того как начнёт разбираться в телах. К моменту, когда он смотрит на вызов boxes(12), имя boxes ему уже известно, где бы оно ни было написано — выше, ниже, в другом файле того же пакета.

Внутри функции правило другое и остаётся прежним:

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

import "fmt"

func main() {
	fmt.Println(qty)
	qty := 5
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:6:14: undefined: qty
./main.go:7:2: declared and not used: qty

Так что порядок функций в файле выбираете вы. Обычный — сверху главное, ниже подробности: сначала main, под ним функции, которые он вызывает, и читатель идёт от общего к частному. Обратный порядок тоже встречается; важно, чтобы внутри одного файла он был один.

Чего нельзя: двух функций с одним именем в одном пакете.

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

import "fmt"

func boxes(qty int) int {
	return qty / 6
}

func boxes(qty int, size int) int {
	return qty / size
}

func main() {
	fmt.Println(boxes(12))
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:9:6: boxes redeclared in this block
	./main.go:5:6: other declaration of boxes

Вторая строка показывает, где объявление было в первый раз. Разное число параметров не помогает: в Go имя занято целиком.

И ещё одно: функцию нельзя объявить внутри другой функции.

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

import "fmt"

func main() {
	func boxes(qty int) int {
		return qty / 6
	}
	fmt.Println(boxes(12))
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/scratch$ go run .
# day11/scratch
./main.go:6:7: syntax error: unexpected name boxes, expected (

Сообщение странное, потому что синтаксис для функции без имени внутри тела в Go есть — но это другая конструкция, и до неё мы дойдём не скоро. Помните признак: func с именем — всегда на верхнем уровне файла, без отступа.

Наконец, опечатка в имени при вызове:

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

import "fmt"

func boxes(qty int) int {
	return qty / 6
}

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

undefined для функции читается так же, как для переменной: «такого имени нет». Смотреть надо не на строку 10, а на объявление: сверить буква в букву.


Порядок аргументов: компилятор промолчит

Ситуация. У функции два параметра одного типа. В вызове их переставили местами.

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

import "fmt"

func boxes(qty int, size int) int {
	n := qty / size
	if qty%size != 0 {
		n++
	}
	return n
}

func main() {
	qty := 40
	size := 6
	fmt.Println("Коробок:", boxes(size, qty))
}

Что увидите.

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

Как это читать. Правильный ответ — 7. Программа собралась, go vet молчит, ошибки нет ни одной: оба аргумента типа int, столько же, сколько параметров, — с точки зрения компилятора всё в порядке. Внутри функции просто посчиталось 6 / 40, то есть 0, плюс единица за остаток.

Совпадение имён не спасает: в main переменная называется size, в функции параметр тоже size, но аргумент size стоит первым и попадает в первый параметр qty. Имена не сопоставляются никогда — только позиции.

Это самый частый тихий дефект дня. Ловится он двумя способами: прочитать вызов вслух вместе с заголовком («boxes от количества и размера») и посчитать ответ руками на маленьком примере — 40 штук по 6 это 7 коробок, а не 1; неправдоподобный результат всегда повод посмотреть на вызов.

Защита от перестановки есть и в языке: параметры разных типов переставить не дадут — будет cannot use … as … value in argument. Но у двух int такой защиты нет.


Попробуйте сейчас: починить fix2.

Цель: fix2 печатает правдоподобное число коробок и остаток; правка — одна-две строки, больше двух проверка не примет.

1. Запустите как есть и прочитайте комментарий в начале файла:

▶ Выполните
cd ~/gocourse/day11/fix2
go run .

2. Посчитайте руками, сколько коробок должно получиться на самом деле. Функцию boxes и числа в main не меняйте — правьте одну-две строки.

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

▶ Выполните
go run .
diff ~/.course/orig/day11/fix2/main.go main.go

Готово, когда: число коробок совпало с вашим расчётом, «в последней» получилось меньше вместимости коробки, а diff показывает одну-две изменённые строки — пункты fix2 и fixsmall.


Словарик ошибок

К концу дня в ~/errors.md — минимум двадцать два разбора: прежние и сегодняшние. Строка засчитывается, если в ней текст ошибки, тире с пробелами вокруг и что это значило; двоеточие не считается — оно есть в каждом сообщении компилятора. Сегодня встречались:


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

Цель: в ~/errors.md не меньше двадцати двух разборов, а course check 11 показывает, что ещё осталось.

1. Допишите сегодняшние ошибки — те, которые вы действительно видели у себя, — и запустите проверку:

▶ Выполните
course check 11

2. Если пункт errors красный, посчитайте разборы так же, как их считает проверка: строка длиннее двадцати символов и с тире-разделителем.

▶ Выполните
grep -E '.{20,}' ~/errors.md | grep -cE ' (-|—|–) '

Первый grep отбрасывает слишком короткие строки, второй считает те, где между текстом ошибки и разбором стоит тире с пробелами вокруг. Обычный grep -c . ~/errors.md покажет больше: он считает все строки подряд, включая заголовки и заметки без разбора. Если числа расходятся, смотреть надо на строки без тире.

Готово, когда: пункт errors в course check 11 зелёный.


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

Что видите Что это значит Что делать
./main.go:10:14: undefined: boxs опечатка в имени функции при вызове сверить с объявлением буква в букву
boxes redeclared in this block и вторая строка other declaration of boxes две функции с одним именем в пакете переименовать одну; разное число параметров не помогает
syntax error: unexpected name boxes, expected ( func имя написано внутри main вынести объявление на верхний уровень, без отступа
undefined: qty и рядом declared and not used: qty переменную читают выше её объявления внутри функции объявить до первого использования
программа считает не то, ошибок нет, оба аргумента int переставлены местами аргументы сверить порядок с заголовком; проверить на примере, ответ которого известен
функция называется calc, f, process имя ничего не говорит существительное для значения, глагол для действия
вызов работает, хотя функция объявлена ниже main так и задумано порядок объявлений на верхнем уровне не важен

Дальше: две задачи целиком — вынести повтор и посчитать стоимость с порогом — course next