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

Объявление и параметры

Функция объявляется словом func, за которым идут имя, параметры в скобках, тип результата и тело:

func greet(name string) string {
    return "привет, " + name
}

Тип пишется после имени параметра - как у переменных. Если у нескольких параметров один тип, его можно указать один раз в конце:

func moveTo(x, y int) {}

Три правила, которые отличают Go от многих языков:

  • Значений по умолчанию нет. Объявили параметр - передавайте его при каждом вызове. Количество аргументов всегда совпадает с количеством параметров.
  • Аргументы передаются по значению: функция получает копию. Изменил параметр внутри - снаружи никто не заметил. Исключение по духу - срезы и map, которые несут внутри указатель на данные: копируется сама «шапка», а данные общие.
  • Неиспользуемый параметр - не ошибка (в отличие от неиспользуемой переменной), но хороший знак, что функция делает лишнее.

Пример с передачей по значению:

live example

package main

import "fmt"

func twice(n int) {
    n = n * 2
}

func main() {
    x := 5
    twice(x)
    fmt.Println(x) // 5: функция меняла копию
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Как всё-таки изменить переменную снаружи - передать указатель: об этом подробно в статье Указатели.

Несколько возвращаемых значенийспросят на собеседовании

Функция может возвращать сразу несколько величин - их перечисляют в скобках:

live example

package main

import "fmt"

func minMax(nums []int) (int, int) {
    min, max := nums[0], nums[0]
    for _, n := range nums {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return min, max
}

func main() {
    lo, hi := minMax([]int{7, 2, 9, 4})
    fmt.Println(lo, hi) // 2 9
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

return возвращает значения списком в том же порядке. Если одно из них не нужно, его отбрасывают подчёркиванием:

_, hi := minMax(nums)

Подчёркивание - «пустая переменная» (blank identifier): присвоить ей можно, использовать нельзя. Чаще всего так отбрасывают именно ошибку _ = возможноОшиблась(), хотя прежде чем писать _, стоит решить: точно ли ошибка не важна.

Это не эмуляция через структуру или массив, как в некоторых языках, а честный механизм языка. На нём держится главный паттерн Go: результат, err := .... Как с ошибкой обращаться - в статье Ошибки.

defer: отложить до выхода из функцииспросят на собеседовании

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

live example

package main

import "fmt"

func main() {
    for i := 1; i <= 3; i++ {
        defer fmt.Println("отложено", i)
    }
    fmt.Println("конец main")
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Программа напечатает «конец main», а потом «отложено 3», «2», «1». Два правила из этого примера: отложенные вызовы выполняются в обратном порядке, и аргументы вычисляются в момент defer, а не в момент выполнения - поэтому каждая строка запомнила своё i. Типичная запись - f, err := os.Open(path), проверка ошибки и сразу defer f.Close().

Третье правило про стоимость: defer внутри цикла выполнится не в конце итерации, а в конце функции, и десять тысяч отложенных Close в цикле это десять тысяч открытых файлов разом. Как отложенная функция может изменить сам результат функции - в следующем разделе. А recover(), который перехватывает панику, работает только внутри отложенной функции - про него в статье Ошибки.

Именованные результаты и deferспросят на собеседовании

Возвращаемым значениям можно дать имена прямо в сигнатуре:

func read() (n int, err error)

Зачем это нужно, если типы и так понятны:

  • Сигнатура становится документацией: сразу видно, что возвращается - count, err, а не «какое-то число».
  • Внутри функции именованные результаты - обычные переменные, уже объявленные нулевым значением. return без аргументов (naked return) отдаёт их текущие значения.

Главную работу именованные результаты делают вместе с defer: отложенная функция выполняется после вычисления результата, но до возврата, и через имя может его изменить:

live example

package main

import (
    "errors"
    "fmt"
)

func parse(items []string) (first string, err error) {
    defer func() {
        if err != nil {
            first = ""
        }
    }()
    if len(items) == 0 {
        return "", errors.New("пустой список")
    }
    first = items[0]
    return first, nil
}

func main() {
    v, err := parse(nil)
    fmt.Println(v == "", err != nil) // true true
    v, err = parse([]string{"го", "руны"})
    fmt.Println(v, err == nil) // го true
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Секция defer обнулила first при ошибке - вызывающий не получит «мусорное» значение вместе с ошибкой. Без имени (func parse() (string, error)) отложенная функция результат изменить не могла бы.

Naked return (return без списка) удобен в коротких функциях; в длинных он прячет, что именно возвращается, - там лучше писать значения явно.

Функции как значения

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

var op func(int, int) int

live example

package main

import (
    "fmt"
    "sort"
)

func main() {
    apply := func(a, b int) int { return a + b }
    fmt.Println(apply(2, 3)) // 5

    nums := []int{5, 2, 8, 1}
    sort.Slice(nums, func(i, j int) bool {
        return nums[i] < nums[j]
    })
    fmt.Println(nums) // [1 2 5 8]
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Переменная apply хранит функцию; sort.Slice принимает сравнение параметром - без передачи функций не было бы ни компараторов сортировки, ни обработчиков HTTP, ни http.HandleFunc("/", handler).

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

type Filter func(int) bool

func keep(nums []int, f Filter) []int {
    var out []int
    for _, n := range nums {
        if f(n) {
            out = append(out, n)
        }
    }
    return out
}

Так сигнатуры читаются легче, а контракт фильтра описан один раз.

Анонимные функции и замыканияспросят на собеседовании

Функция без имени - функциональный литерал - объявляется на месте и чаще всего сразу вызывается или передаётся:

func(a, b int) int { return a * b }

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

live example

package main

import "fmt"

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    next := counter()
    fmt.Println(next(), next(), next()) // 1 2 3
    other := counter()
    fmt.Println(other()) // 1: у каждой функции своя переменная
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

next и other вернулись из разных вызовов counter - и у каждой своя копия count. Переменная живёт, пока жива хотя бы одна функция, которая её захватила.

Одна классическая грабля: замыкание в цикле захватывает саму переменную, а не её значение на итерации. До Go 1.22 это стреляло в горутинах - все замыкания видели последнее значение. С Go 1.22 переменная цикла создаётся заново на каждой итерации, но правило осталось тем же: замыкание помнит переменную, а не снимок значения. Подробнее - в статье Горутины и каналы.

Рекурсия

Функция может вызывать сама себя - классический пример, факториал:

live example

package main

import "fmt"

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

func main() {
    fmt.Println(factorial(5)) // 120
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

У рекурсии всегда две части: базовый случай (n <= 1), который останавливает спуск, и шаг, сводящий задачу к меньшей. Без базового случая функция уйдёт в бесконечный спуск и уронит программу.

Для рекурсии в Go есть ограничение по стеку, но на практике раньше закончится другая беда: рекурсивное решение часто тратит лишнюю память и время там, где хватит цикла. Факториал и суммы в реальном коде пишут циклом; рекурсия уместна там, где данные сами рекурсивны - деревья, вложенные структуры, обход каталогов.

Вариативные функции

Функция может принимать сколько угодно аргументов одного типа - последняя ссылка объявляется с многоточием: func sum(nums ...int) int. Внутри nums - обычный срез, а готовый срез передают с многоточием после имени: sum(values...). Так устроены fmt.Println и append. Подробно с примерами - в разделе «Глубже» статьи Синтаксис и типы.

Коротко

  • Тип пишется после имени: func greet(name string) string. Значений по умолчанию нет, аргументы передаются копией.
  • Функция возвращает несколько значений: return min, max; ненужное отбрасывают через _.
  • defer откладывает вызов до выхода из функции: отложенное выполняется в обратном порядке, аргументы вычисляются в момент defer, а копится всё до конца функции.
  • Именованные результаты делают сигнатуру документацией и позволяют defer изменить результат до возврата; naked return - только в коротких функциях.
  • Функция - значение: тип func(int, int) int, присваивание переменной, передача параметром, свой тип через type Filter func(int) bool.
  • Анонимная функция с захваченными переменными - замыкание; оно помнит переменную, а не её значение на момент создания.
  • Рекурсия требует базового случая; для линейных задач обычно хватает цикла.
  • Вариативность задаётся многоточием: sum(nums ...int), готовый срез передают как nums....

Что почитать дальше