Функции

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

До сих пор мы писали программы, используя лишь одну функцию:

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).

  • Что такое отложенный вызов, паника и восстановление? Как восстановить функцию после паники?

Fork me on GitHub