Функция является независимой частью кода, связывающей один или несколько входных параметров с одним или несколькими выходными параметрами. Функции (также известные как процедуры и подпрограммы) можно представить как черный ящик:

До сих пор мы писали программы, используя лишь одну функцию:
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).Что такое отложенный вызов, паника и восстановление? Как восстановить функцию после паники?