Функция - основной строительный блок 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....
Что почитать дальше
- Ошибки в Go - как множественный возврат стал главной идиомой языка.
- Строки, руны и пакет strings - строки как пример неизменяемых значений и передачи по значению.
- Горутины и каналы - замыкания в цикле и главная грабля с ними.