Функция — это логически целостный участок кода с одним входом и одним выходом в потоке управления. Этот участок можно использовать многократно, обращаясь к нему по имени.
Смысловая нагрузка у функции в программировании примерно такая же, как у функции в математике. У функции есть название и определение. Ей можно передать значения переменных и получить результат.
Сначала нужно определить функцию:
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