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

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