Функция является независимой частью кода, связывающей один или несколько входных параметров с одним или несколькими выходными параметрами. Функции (также известные как процедуры и подпрограммы) можно представить как черный ящик:
До сих пор мы писали программы, используя лишь одну функцию:
func main() {}
Но сейчас мы начнем создавать код, содержащий более одной функции.
Ваша вторая функция
Вспомните эту программу из предыдущей главы:
func main() {
xs := []float64{98,93,77,82,83}
total := 0.0
for _, v := range xs {
total += v
}
fmt.Println(total / float64(len(xs)))
}
Эта программа вычисляет среднее значение ряда чисел. Поиск среднего значения — основная задача и идеальный кандидат для вынесения в отдельную функцию.
Функция average
должна взять срез из нескольких float64
и вернуть один
float64
. Напишем перед функцией main
:
func average(xs []float64) float64 {
panic("Not Implemented")
}
Функция начинается с ключевого слова func
, за которым следует имя функции.
Аргументы (входы) определяются так: имя тип, имя тип, …
. Наша функция имеет
один параметр (список оценок) под названием xs
. За параметром следует
возвращаемый тип. В совокупности аргументы и возвращаемое значение также
известны как сигнатура функции.
Наконец, далее идет тело функции, заключенное в фигурные скобки. В теле вызывается
встроенная функция panic
, которая вызывает ошибку выполнения (о ней я расскажу
чуть позже в этой главе). Процесс написания функций может быть сложен, поэтому
деление этого процесса на несколько частей вместо попытки реализовать всё за
один большой шаг — хорошая идея.
Теперь давайте перенесём часть кода из функции main
в функцию average
:
func average(xs []float64) float64 {
total := 0.0
for _, v := range xs {
total += v
}
return total / float64(len(xs))
}
Обратите внимание, что мы заменили вызов fmt.Println
на оператор return
.
Оператор возврата немедленно прервет выполнение функции и вернет значение,
указанное после оператора, в функцию, которая вызвала текущую. Приведем main
к
следующему виду:
func main() {
xs := []float64{98,93,77,82,83}
fmt.Println(average(xs))
}
Запуск этой программы должен дать точно такой же результат, что и раньше. Несколько моментов, которые нужно иметь ввиду:
имена аргументов не обязательно должны совпадать с именами переменных при вызове функции. Например, можно сделать так:
func main() { someOtherName := []float64{98,93,77,82,83} fmt.Println(average(someOtherName)) }
и программа продолжит работать;
функции не имеют доступа к области видимости родительской функции, то есть это не сработает:
func f() { fmt.Println(x) } func main() { x := 5 f() }
Как минимум нужно сделать так:
func f(x int) { fmt.Println(x) } func main() { x := 5 f(x) }
или так:
var x int = 5 func f() { fmt.Println(x) } func main() { f() }
функции выстраиваются в «стек вызовов». Предположим, у нас есть такая программа:
func main() { fmt.Println(f1()) } func f1() int { return f2() } func f2() int { return 1 }
Её можно представить следующим образом:
Каждая вызываемая функция помещается в стек вызовов, каждый возврат из функции возвращает нас к предыдущей приостановленной подпрограмме;
можно также явно указать имя возвращаемого значения:
func f2() (r int) { r = 1 return }
Возврат нескольких значений
Go способен возвращать несколько значений из функции:
func f() (int, int) {
return 5, 6
}
func main() {
x, y := f()
}
Для этого необходимы три вещи: указать несколько типов возвращаемых значений,
разделенных ,
, изменить выражение после return
так, чтобы оно содержало
несколько значений, разделенных ,
, и, наконец, изменить конструкцию присвоения
так, чтобы она содержала несколько значений в левой части перед :=
или =
.
Возврат нескольких значений часто используется для возврата ошибки вместе с
результатом (x, err := f()
) или логического значения, говорящего об успешном
выполнении (x, ok := f()
).
Переменное число аргументов функции
Существует особая форма записи последнего аргумента в функции Go:
func add(args ...int) int {
total := 0
for _, v := range args {
total += v
}
return total
}
func main() {
fmt.Println(add(1,2,3))
}
Использование ...
перед типом последнего аргумента означает, что функция может
содержать ноль и более таких параметров. В нашем случае мы берем ноль и более
int
. Функцию можно вызывать, как и раньше, но при этом ей можно передать любое
количество аргументов типа int
.
Это похоже на реализацию функции Println
:
func Println(a ...interface{}) (n int, err error)
Функция Println
может принимать любое количество аргументов любого типа (тип
interface
мы рассмотрим в главе 9).
Мы также можем передать срез int
-ов, указав ...
после среза:
func main() {
xs := []int{1,2,3}
fmt.Println(add(xs...))
}
Замыкания
Возможно создавать функции внутри функций:
func main() {
add := func(x, y int) int {
return x + y
}
fmt.Println(add(1,1))
}
add
является локальной переменной типа func(int, int) int
(функция принимает
два аргумента типа int
и возвращает int
). При создании локальная функция
также получает доступ к локальным переменным (вспомните области видимости из
главы 4):
func main() {
x := 0
increment := func() int {
x++
return x
}
fmt.Println(increment())
fmt.Println(increment())
}
increment
прибавляет 1
к переменной x
, которая определена в рамках функции
main
. Значение переменной x
может быть изменено в функции increment
. Вот
почему при первом вызове increment
на экран выводится 1
, а при втором — 2
.
Функцию, использующую переменные, определенные вне этой функции, называют
замыканием. В нашем случае функция increment
и переменная x
образуют
замыкание.
Один из способов использования замыкания — функция, возвращающая другую функцию, которая при вызове генерирует некую последовательность чисел. Например, следующим образом мы могли бы сгенерировать все четные числа:
func makeEvenGenerator() func() uint {
i := uint(0)
return func() (ret uint) {
ret = i
i += 2
return
}
}
func main() {
nextEven := makeEvenGenerator()
fmt.Println(nextEven()) // 0
fmt.Println(nextEven()) // 2
fmt.Println(nextEven()) // 4
}
makeEvenGenerator
возвращает функцию, которая генерирует чётные числа. Каждый
раз, когда она вызывается, к переменной i
добавляется 2
, но в отличие от
обычных локальных переменных её значение сохраняется между вызовами.
Рекурсия
Наконец, функция может вызывать саму себя. Вот один из способов вычисления факториала числа:
func factorial(x uint) uint {
if x == 0 {
return 1
}
return x * factorial(x-1)
}
factorial
вызывает саму себя, что делает эту функцию рекурсивной. Для того,
чтобы лучше понять, как работает эта функция, давайте пройдемся по
factorial(2)
:
-
x == 0
? Нет. (x
равен2
); - ищем факториал от
x - 1
;-
x == 0
? Нет. (x
равен1
);
-
- ищем факториал от
0
;-
x == 0
? Да, возвращаем1
;
-
- возвращаем
1 * 1
; - возвращаем
2 * 1
.
Замыкание и рекурсивный вызов — сильные техники программирования, формирующие основу парадигмы, известной как функциональное программирование. Большинство людей находят функциональное программирование более сложным для понимания, чем подход на основе циклов, логических операторов, переменных и простых функций.
Отложенный вызов, паника и восстановление
В Go есть специальный оператор defer
, который позволяет отложить вызов
указанной функции до тех пор, пока не завершится текущая. Рассмотрим следующий
пример:
package main
import "fmt"
func first() {
fmt.Println("1st")
}
func second() {
fmt.Println("2nd")
}
func main() {
defer second()
first()
}
Эта программа выводит 1st
, затем 2nd
. Грубо говоря defer
перемещает вызов
second
в конец функции:
func main() {
first()
second()
}
defer
часто используется в случаях, когда нужно освободить ресурсы после
завершения. Например, открывая файл необходимо убедиться, что позже он должен
быть закрыт. C defer
это выглядит так:
f, _ := os.Open(filename)
defer f.Close()
Такой подход дает нам три преимущества: (1) вызовы Close
и Open
располагаются рядом, что облегчает понимание программы, (2) если функция
содержит несколько операций возврата (например, одна произойдет в блоке if
,
другая в блоке else
), Close
будет вызван до выхода из функции, (3)
отложенные функции вызываются, даже если во время выполнения происходит ошибка.
Паника и восстановление
Ранее мы создали функцию, которая вызывает panic
, чтобы сгенерировать ошибку
выполнения. Мы можем обрабатывать паники с помощью встроенной функции recover
.
Функция recover
останавливает панику и возвращает значение, которое было
передано функции panic
. Можно попытаться использовать recover
следующим
образом:
package main
import "fmt"
func main() {
panic("PANIC")
str := recover()
fmt.Println(str)
}
Но в данном случае recover
никогда не будет вызвана, поскольку вызов panic
немедленно останавливает выполнение функции. Вместо этого мы должны использовать
его вместе с defer
:
package main
import "fmt"
func main() {
defer func() {
str := recover()
fmt.Println(str)
}()
panic("PANIC")
}
Паника обычно указывает на ошибку программиста (например, попытку получить доступ к несуществующему индексу массива, забытая и непроинициализированная карта и т.д.) или неожиданное поведение (исключение), которое нельзя обработать (поэтому оно и называется «паника»).
Задачи
Функция
sum
принимает срез чисел и складывает их вместе. Как бы выглядела сигнатура этой функции?Напишите функцию, которая принимает число, делит его пополам и возвращает
true
в случае, если исходное число чётное, иfalse
, если нечетное. Например,half(1)
должна вернуть(0, false)
, в то время какhalf(2)
вернет(1, true)
.Напишите функцию с переменным числом параметров, которая находит наибольшее число в списке.
Используя в качестве примера функцию
makeEvenGenerator
напишитеmakeOddGenerator
, генерирующую нечётные числа.Последовательность чисел Фибоначчи определяется как
fib(0) = 0
,fib(1) = 1
,fib(n) = fib(n-1) + fib(n-2)
. Напишите рекурсивную функцию, находящуюfib(n)
.Что такое отложенный вызов, паника и восстановление? Как восстановить функцию после паники?