Функции

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

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

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

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

results matching ""

    No results matching ""