Функция — это логически целостный участок кода с одним входом и одним выходом в потоке управления. Этот участок можно использовать многократно, обращаясь к нему по имени.

Смысловая нагрузка у функции в программировании примерно такая же, как у функции в математике. У функции есть название и определение. Ей можно передать значения переменных и получить результат.

Сначала нужно определить функцию:

func Cube(x int) int { // декларация функции
    return x * x * x  // тело функции
} 

И только затем её можно использовать:

result := Cube(5) // вызов функции 

Декларация функции в Go

Декларацию функции часто называют сигнатурой (signature).

func MyFunction(arg1 arg1type, arg2 arg2type) resultType {
    // тело функции
} 

Здесь:

  • MyFunction — имя функции.
  • arg1 arg1type — параметр функции и его тип. Типы параметров должны быть заявлены при декларации, потому что Go — статически типизированный язык.
  • resultType — тип возвращаемого значения.

Результат функции тоже можно именовать:

func Divide(x int) (half int) {
    half = x / 2
    return // тогда в инструкции return имя можно не указывать
} 

Note

Обратите внимание, что нельзя декларировать функцию внутри другой функции. Это ограничение языка, связанное с особенностями компиляции.

Параметры

Аргументы передаются функции путём копирования значения, то есть функции не могут изменять переданные им переменные.

Для примера напишем такой код:

func increment(x int) {
    // x — локальная переменная для этой функции
    x++ 
}
 
func main() {
    n := 5
    // n копируется в переменную x
    increment(n) // значение n не изменится
    fmt.Println(n)
} 

Получим:

5 

Если параметры одного типа, можно сократить код:

func Sum(x, y int) int{
    return x + y
} 

Note

В Go есть специальный синтаксис для функций, которые можно вызывать с переменным количеством аргументов (variadic functions). Параметр, принимающий такие аргументы, нужно поставить последним в списке, а перед его типом — многоточие.

func Sum(x ...int) int 

Внутри функции этот параметр рассматривается как нумерованная последовательность аргументов (slice).

func Sum(x ...int) (res int) {
    for _, v := range x {
        res += v
    }
    return
} 

Вызывают такую функцию обычным образом, со списком аргументов через запятую:

sum := Sum(2, 3, 5, 1, 2, 57) 

Если вызвать эту функцию без аргументов Sum(), параметр x примет значение nil. Тогда цикл не пройдёт ни одной итерации, и функция вернёт 0.

Возвращаемые значения

Функция необязательно возвращает значение. Она может использоваться исключительно ради побочных эффектов, производимых ею в среде исполнения. Например, fmt.Println().

Также функция может возвращать не одно, а несколько значений разных типов.

func foo() (int, int, string) 

При вызове такой функции должны быть предоставлены переменные, которым надо все эти значения присвоить.

x, y, z := foo() 

А если некоторые значения не нужны, можно воспользоваться переменной _.

_, y, _ := foo() 

В функции, которая возвращает значение, обязательно должна быть инструкция return.

Список возвращаемых значений имеет тот же синтаксис, что и список параметров. Например, можно написать коротко:

func foo() (x, y, z int) 

Обратите внимание, что здесь возвращаемые значения имеют имена. В теле функции они могут использоваться как обычные переменные. При входе в функцию они будут инициализированы значениями по умолчанию для данного типа.

Если функция содержит именованные возвращаемые значения, то необязательно указывать список возвращаемых значений оператором return. В таком случае будут возвращены значения переменных, которые они имеют к этому моменту.

Вот функция, которая находит индекс буквы в строке и возвращает вторым аргументом false, если буква не найдена:

func Index(st string, a rune) (index int, ok bool) {
    for i, c := range st {
        if c == a {
            return i, true
        }
    }
    return // вернутся значения по умолчанию
} 

Если количество и тип возвращаемых функцией значений

func foo() (int, int) 

в точности соответствуют параметрам другой функции,

func bar(x int, y int) 

то допускается такой синтаксис вызова:

bar(foo()) 

Рекурсивные функции

В#Go можно декларировать рекурсивную функцию — вызывающую саму себя.

Вот хрестоматийный пример рекурсивного вычисления n!, факториала числа:

func fact(n int) int {
    if n == 0 {    // терминальная ветка — то есть условие выхода из рекурсии
        return 1
    } else {    // рекурсивная ветка 
        return n * fact(n-1)
    }
} 

А вот числа Фибоначчи:

func Fib(n int) int {
    switch {
    case n <= 1:    // терминальная ветка 
        return n
    default:        // рекурсивная ветка
        return Fib(n-1) + Fib(n-2)
    }
} 

Следует помнить, что в Go вызов функции имеет определённую вычислительную стоимость, а также затраты по памяти, ведь как минимум нужно скопировать аргументы. Поэтому множество вложенных вызовов функции может привести к снижению производительности программы и переполнению памяти.

Итеративные алгоритмы будут работать быстрее. Для сравнения приведём итеративную реализацию (на основе циклов) вышеуказанных примеров:

func fact(n int) int {
    res := 1
    for n > 0 {
        res *= n
        n--
    }
    return res
} 
func Fib(n int) int {
    a, b := 0, 1
    for n > 0 {
        a, b = b, a+b
        n--
    }
    return a
} 

Тем не менее это не означает, что рекурсивные алгоритмы неприменимы. В ряде случаев они могут быть полезнее, проще и делать код нагляднее.

Приведём пример работы с рекурсивным обходом всех файлов в данной директории, причём директория может содержать вложенные поддиректории:

package main
 
import (
    "fmt"
    "os"
    "path/filepath"
)
 
func main() {
    PrintAllFiles(".")
}
 
func PrintAllFiles(path string) {
    // получаем список всех элементов в папке (и файлов, и директорий)
    files, err := os.ReadDir(path)
    if err != nil {
        fmt.Println("unable to get list of files", err)
        return
    }
    //  проходим по списку
    for _, f := range files {
        // получаем имя элемента
        // filepath.Join — функция, которая собирает путь к элементу с разделителями
        filename := filepath.Join(path, f.Name())
        // печатаем имя элемента
        fmt.Println(filename)
        // если элемент — директория, то вызываем для него рекурсивно ту же функцию
        if f.IsDir() {
            PrintAllFiles(filename)
        }
    }
} 

Итеративная реализация данного алгоритма была бы куда сложнее.

Функция первого класса

Функции в Go ничем не уступают другим классам объектов. У функции есть тип и значение. Функцию можно присвоить переменной, можно передать аргументом другой функции. Функция может возвращать в качестве значения другую функцию.

Тип функции виден в её сигнатуре, то есть определяется как набор типов и количества аргументов, возвращаемых значений.

Например, эта функция

func Say(animal string) (v string) {
    switch animal {
    default:
        v = "heh"
    case "dog":
        v = "gav"
    case "cat":
        v = "myau"
    case "cow":
        v = "mu"
    }
    return
} 

имеет тип:

func(string) string 

Можно присвоить её переменной такого типа:

var voice func(string) string
voice = Say 

Можно написать функцию высшего порядка с параметром такого типа:

func Print(who string, how func(string) string){
    fmt.Println(how(who))
} 

И передать ей функцию аргументом:

Print("dog", Say) 

Для функции есть литеральная форма синтаксиса. Функцию можно создать по месту, не декларируя и не именуя в блоке деклараций.

f := func(s string) string { return s } 

Можно даже использовать литерал в качестве аргумента при вызове:

Print("dog", func(s string) string { return s }) 

Это то, что ещё называют анонимной или лямбда-функцией.

Можно написать функцию, которая возвращает функции значениями:

func Do(say bool) func(string) string {
    if say {
        return Say
    }
    return func(s string) string { return s }
} 

И вызывать вот так:

Print("dog", Do(true)) 

Замыкания

Go — язык с лексической областью видимости (lexically scoped). Это значит, что переменные, определённые в окружающих блоках видимости (например, глобальные переменные), доступны функции всегда, а не только на время вызова. Можно считать, что функция их запоминает.

Лексическая область видимости и анонимные функции позволяют реализовать замыкания (closure).

Вот классический пример итератора чётных чисел, построенного на замыкании:

func Generate(seed int) func() {
    return func() {
        fmt.Println(seed) // замыкание получает внешнюю переменную seed
        seed += 2 // переменная модифицируется
    }
    
}
 
func main() {
    iterator := Generate(0)
    iterator()
    iterator()
    iterator()
    iterator()
    iterator()
} 

Замыкание привязывает к себе внешнюю переменную. После выхода из внешней функции Generate она не уничтожается, а остаётся привязанной к функции замыкания, причём её значение сохраняется между вызовами функции.

Получаем:

0
2
4
6
8 

А вот и упоминавшиеся числа Фибоначчи, но теперь написанные с применением замыкания:

func fib() func() int {
    x1, x2 := 0, 1
    // возвращаемая функция замыкает x1, x2
    return func() int {
        x1, x2 = x2, x1+x2
        return x1
    }
}
 
func main() {
    f := fib() // получили функцию-замыкание. f() — захватила x1, x2. x1 = 0, x2 = 1
    fmt.Println(f()) // x1 = 1, x2 = 1
    fmt.Println(f()) // x1 = 1, x2 = 2
    fmt.Println(f()) // x1 = 2, x2 = 3
    fmt.Println(f()) // x1 = 3, x2 = 5
    fmt.Println(f()) // x1 = 5, x2 = 8
    fmt.Println(f()) // x1 = 8, x2 = 13
 
} 

Получаем:

1
1
2
3
5
8
13 

Такие функции иногда называют генераторами. Они выдают новое значение какой-либо последовательности при каждом вызове.

Замыкания довольно полезные. Они позволяют просто и изящно реализовать определённые паттерны проектирования. Тем не менее, чтобы эффективно использовать замыкания, надо представлять, как они работают.

Приведём более практичный пример использования замыкания. Создадим две функции-обёртки, одна из которых будет подсчитывать количество вызовов, а вторая — время исполнения функции.

// countCall — функция-обёртка для подсчёта вызовов
func countCall(f func(string)) func(string) {
    cnt := 0
    // получаем имя функции. Подробнее об этом вызове будет рассказано в следующем курсе
    funcname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
    return func(s string) {
        cnt++
        fmt.Printf("Функция %s вызвана %d раз\n", funcname, cnt)
        f(s)
    }
}
 
// metricTimeCall — функция-обёртка для замера времени
func metricTimeCall(f func(string)) func(string) {
    return func(s string) {
        start := time.Now() // получаем текущее время
        f(s)
        fmt.Println("Execution time: ", time.Now().Sub(start)) // получаем интервал времени как разницу между двумя временными метками
    }
}
 
func myprint(s string) {
    fmt.Println(s)
}
 
func main() {
 
    countedPrint := countCall(myprint)
    countedPrint("Hello world")
    countedPrint("Hi")
 
    // обратите внимание, что мы оборачиваем уже обёрнутую функцию, а значение счётчика вызовов при этом сохраняется
    countAndMetricPrint := metricTimeCall(countedPrint)
    countAndMetricPrint("Hello")
    countAndMetricPrint("World")
 
}
// Результат
 
Функция main.myprint вызвана 1 раз
Hello world
Функция main.myprint вызвана 2 раз
Hi
Функция main.myprint вызвана 3 раз
Hello
Execution time:  3.147µs
Функция main.myprint вызвана 4 раз
World
Execution time:  3.16µs

Note

Такой подход часто применяется в веб-разработке на Go, когда группа функций-обработчиков объединяется в цепочки, разделяя между собой ответственность за определённые действия.

Особенные функции

Точка входа в программу — функция main(). Она обязательно должна существовать в единственном виде и в любой исполняемой программе на Go. main() не принимает аргументов и не возвращает значений.

В Go есть встроенные функции, например: make(), new(), len(), cap(), delete(), close(), append(), copy(), panic(), recover(). Это не библиотечные функции. Они не вполне подчиняются правилам для функций пользователя. У них может не быть сигнатуры, а их использование документировано в спецификации языка — основополагающем для Go документе.

В базовом синтаксисе языка также описана вот эта функция:

func init() { … } 

В пакете и даже в одном файле можно декларировать несколько таких функций. Они будут вызваны один раз при инициализации пакета, после присвоения глобальных переменных, в том порядке, в котором они предоставлены компилятору (встречаются в исходном тексте). Прямой вызов функции init() в коде программы не предусмотрен.

Служат эти функции для создания окружения, необходимого пакету для корректной работы.

Вот простой пример:

var name, surname string
 
func init() {
    name = "John"
}
func init() {
    if surname == "" {
        surname = "Doe"
    }
}
func main() {
    fmt.Println("Hello " + name + " " + surname)
} 

Конструктор с опциями (funcopts)

Напишем конструктор типа с начальными значениями и удобными опциями. Воспользуемся подходом, который предложил Роб Пайк в статье Self-referential functions and the design of options.

В Go нет конструкторов в классическом ООП-понимании. Есть встроенные аллокаторы make() и new(), которые инициализируют поля в их нулевые значения. Необходимые параметры устанавливаются литерально, присваиванием.

type Person struct {
    Name string
    Surname string
    Age int
}
 
john := Person{Name: "John", Surname: "Doe", Age: 21}
john.Age = 27 

Это не очень технологично. Предположим, нам нужно инициализировать однотипные элементы значениями по умолчанию, но с возможностью задать некоторые параметры.

type Item struct {
    NoOption string
    Parameter1 string
    Parameter2 int
}
 

Сделаем свой конструктор с опциями.

func NewItem(opts ...option) *Item {
    // инициализируем типовыми значениями
    i := &Item{
        NoOption: "usual",
        Parameter1: "default",
        Parameter2: 42,
    }
    // применяем опции в том порядке, в котором они были заявлены
    for _, opt := range opts {
        opt(i)
    }
    return i
} 

Здесь опции — это функции, применяемые к объекту. За это подход получил название funcopts.

type option func(*Item) 

Чтобы устанавливать параметры, будем использовать функции высшего порядка, возвращающие значениями функции option.

func Option1(option1 string) option {
    return func(i *Item) {
        i.Parameter1 = option1
    }
}
func Option2(option2 int) option {
    return func(i *Item) {
        i.Parameter2 = option2
    }
} 

Тогда инициализация объекта конструктором будет выглядеть так:

func main() {
    // с параметрами по умолчанию
    item1 := NewItem()
    // с применением одной опции
    item2 := NewItem(Option2(70))
    // или двух
    item3 := NewItem(Option1("unusual"), Option2(99))
    // опции можно заявлять в разном порядке
    item4 := NewItem(Option2(88), Option1("rare"))
} 

📂 Go | Последнее изменение: 20.08.2024 15:05