Shanraq.org Shanraq.org
Функции в Go: параметры, return и два значения сразу
IT

Go: с нуля до своего блога Урок 5 из 50

Функции в Go: параметры, return и два значения сразу

Пятый урок курса по Go. Как объявить функцию и что такое параметр, аргумент и return. Главное в уроке: функция в Go возвращает два значения сразу — результат и признак, получилось ли. Отсюда и отсутствие исключений в языке. Разбираем приём result, ok := и подчёркивание.

Зачем это нужно

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

Но главное в уроке другое. В Go функция умеет вернуть две вещи сразу: результат и признак, получилось ли. На этом стоит вся работа с ошибками в языке: привычного try/catch в Go нет, ошибка — обычное возвращаемое значение. Механизм для действительно исключительных случаев (panic) есть, но он не для этого.

Сразу целиком

Новая папка, go mod init sabaq04, main.go:

package main

import "fmt"

func greet(name string) string {
	return "Привет, " + name + "!"
}

func divide(a, b float64) (float64, bool) {
	if b == 0 {
		return 0, false
	}
	return a / b, true
}

func main() {
	fmt.Println(greet("мир"))

	result, ok := divide(10, 4)
	fmt.Println("10 / 4 =", result, "· получилось:", ok)

	result, ok = divide(10, 0)
	fmt.Println("10 / 0 =", result, "· получилось:", ok)
}

Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте: расхождение и есть то место, где вы ещё не знаете языка.

go run . печатает:

Привет, мир!
10 / 4 = 2.5 · получилось: true
10 / 0 = 0 · получилось: false

Обратите внимание на последнюю строку. Уточним сразу, чтобы не осталось ложного впечатления: Go и без нашей проверки не упал бы. Деление float64 на ноль в Go не авария — оно даёт +Inf, -Inf или NaN. Проверка нужна не чтобы спасти программу, а чтобы не отдать дальше бессмысленное число: +Inf спокойно доедет до страницы и покажется читателю.

Разбор

Как читается объявление

func greet(name string) string {

Слева направо: func — начинается функция; greet — её имя; (name string) — что нужно принести и как это будет называться внутри; string после скобок — что получится на выходе.

Образ. Бланк заказа в мастерской. Сверху графа «что вы приносите», снизу графа «что заберёте». Пока бланк не заполнили и не подали, мастер ничего не делает.

Параметр и аргумент

Это два разных слова, и их постоянно путают.

Параметрname в объявлении. Это пустое место, заготовленное под будущее значение.

Аргумент"мир" в вызове greet("мир"). Это то, что вы в это место положили.

Образ. Графа в бланке и то, что вы в неё вписали. Графа всегда одна и та же, а вписывают в неё каждый раз разное.

return

return заканчивает работу функции и отдаёт результат наружу. Всё, что написано после него, уже не выполнится.

Образ. Отдать готовое в окно выдачи и закрыть заказ. После выдачи мастер к этому заказу не возвращается, даже если в тетради ниже осталась запись.

Два значения вместо исключений

Вот главное место урока:

func divide(a, b float64) (float64, bool) {

Круглые скобки вокруг двух типов означают: функция отдаёт две вещи сразу. Первая — результат деления, вторая — получилось ли.

Почему так. Деление на ноль — это не поломка программы, а обычный случай, который надо предусмотреть. В других языках такое принято «выбрасывать» отдельным механизмом, и его легко не поймать. Go не прячет неудачу в сторону, а кладёт её вам прямо в руки вторым значением.

Образ. Справка из окна. Вам выдают не только бумагу, но и отметку: «выдана» или «отказано». Одно без другого не дают — отметка приходит к вам вместе со справкой, и чтобы её не увидеть, надо отвернуться нарочно. Ниже посмотрим, как это выглядит в коде.

Как принять два значения

Слева от := пишут два имени через запятую:

result, ok := divide(10, 4)

Порядок важен: первым идёт результат, вторым признак. Имена вы выбираете сами, и компилятор их не проверяет: напишете ok, result := divide(10, 4) — программа соберётся, просто в ok окажется число, а в result — да-нет. Смысл задаёт порядок, а не название.

Второе имя почти всегда называют ok, а когда речь пойдёт про настоящие ошибки — err. Это не правило языка, а общая привычка, благодаря которой чужой код читается с первого взгляда.

Если второе значение не нужно

Просто пропустить его нельзя: Go требует принять всё, что функция вернула. Для «мне это не нужно» есть подчёркивание:

result, _ := divide(10, 4)

Образ. Графа в бланке, которую вы перечёркиваете. Оставить пустой нельзя — бланк не примут. А перечеркнуть можно: видно, что графу заметили и осознанно пропустили.

С ok так делать не стоит почти никогда: вы своими руками выбрасываете единственное сообщение о том, что что-то пошло не так.

Несколько параметров одного типа

func divide(a, b float64) (float64, bool)

a и b оба float64, и тип написан один раз, в конце. Если типы разные — пишут каждый: func post(title string, views int).

Функция, которая возвращает функцию

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

func counter() func() int {
	n := 0
	return func() int {
		n = n + 1
		return n
	}
}

func main() {
	next := counter()
	fmt.Println(next())
	fmt.Println(next())
	fmt.Println(next())

	other := counter()
	fmt.Println("другой счётчик:", other())
}
1
2
3
другой счётчик: 1

Читается объявление так же, как раньше: func counter() — функция без параметров, а её тип возврата — func() int, то есть «функция без параметров, возвращающая целое».

Теперь главное. n объявлена внутри counter, и по всем прежним правилам должна была исчезнуть, когда counter закончилась. Но она жива: возвращённая функция её помнит и правит при каждом вызове. Такую функцию называют замыканием — она замкнула на себе переменную из той функции, где родилась.

Последняя строка вывода — проверка, что это не общая переменная на всех: у второго счётчика своя n, и он начинает с единицы.

Образ. Записка в кармане. Функция ушла из дома, где родилась, но унесла с собой листок с числом — и правит именно свой листок, а не чей-то чужой.

Пугаться записи не нужно, вы будете видеть её часто. В уроке про обёртки вы напишете функцию, которая берёт обработчик и возвращает новый, — она устроена ровно так же, и n там заменит то, что обёртка должна помнить.

Карта урока

Карта урока: один вызов — два ответа

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Чем параметр отличается от аргумента?
  2. Почему divide возвращает bool, а не просто останавливает программу при делении на ноль?
  3. Что произойдёт, если написать result := divide(10, 4) — с одним именем слева?

Задание

Обязательное. Напишите функцию readingTime(words int) int, которая считает время чтения статьи из расчёта 200 слов в минуту, округляя вверх: (words + 199) / 200. Целое деление отбрасывает остаток, поэтому 199 прибавляют до деления: неполная минута всё равно идёт в счёт, а ровные 400 слов дают 2, а не 3. Вызовите функцию из main для 150, 400 и 1000 слов и сверьте ответы: должны получиться 1, 2 и 5. У нуля слов время чтения нулевое, и это правильно.

По желанию.

  • Сделайте, чтобы readingTime возвращала два значения: минуты и bool — разумное ли число слов. Для проверки понадобится if, а его мы разберём в следующем уроке: если пока не получается, вернитесь к этому заданию после него.
  • Замените ok на _ в вызове divide(10, 0) и посмотрите, что напечатает программа. Вот так и теряются ошибки.
  • Напишите функцию без return и с типом результата в объявлении. Прочитайте, что скажет Go.

Куда это встанет в блоге

Многие функции в вашем блоге будут возвращать два значения: статью и «нашлась ли», текст и «удалось ли разобрать», соединение с базой и «получилось ли подключиться». В уроке про ошибки вторым значением станет не bool, а error, и это уже настоящий рабочий приём Go.

Ответы

Показать ответы
  1. Параметр — имя в объявлении функции, пустое место под значение. Аргумент — конкретное значение, которое вы подставили при вызове. Параметр один и тот же всегда, аргументы разные каждый раз.
  2. Потому что деление на ноль здесь не авария: Go вернул бы +Inf и поехал дальше. Именно это и опасно — бессмысленное число разошлось бы по программе молча. Вернув признак, функция отдаёт решение тому, кто её вызвал, вместо того чтобы притворяться, что ответ получен.
  3. Программа не соберётся. Go скажет что-то вроде assignment mismatch: 1 variable but divide returns 2 values: язык не позволяет молча потерять возвращённое значение, для отказа от него есть подчёркивание.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.