Функции в Go: параметры, возврат значений и замыкания

• 12 мин чтения

Функция в Go объявляется через func, принимает параметры и возвращает ноль, одно или несколько значений. Например, func add(a, b int) int принимает два целых числа и возвращает их сумму. Функцию можно передать в другую функцию, а замыкание позволяет сохранить доступ к внешним переменным.

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

Объявление и вызов функции в Go

Полная программа с функцией сложения:

package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	result := add(2, 3)
	fmt.Println(result) // 5
}

В объявлении func add(a, b int) int:

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

Запись a, b int означает, что оба параметра имеют тип int. Можно написать длиннее: a int, b int. Если типы разные, их указывают отдельно: func repeat(text string, count int) string.

Параметр a объявлен в функции. Аргумент 2 передан при вызове add(2, 3). При вызове a получает значение 2, а b получает 3.

Выражение add обозначает саму функцию. Скобки вызывают её: add(2, 3).

Если функция ничего не возвращает, тип результата не указывают:

func printMessage(message string) {
	fmt.Println(message)
}

В такой функции можно использовать пустой return для досрочного выхода. В конце тела писать его не требуется. Типа void для этого в Go нет.

В коротких примерах ниже опущены package main, импорты и main. Для вывода подключите fmt, для преобразования строки в число подключите strconv. Именованные функции объявляйте на уровне пакета, строки с вызовами размещайте внутри main.

Экспорт функций, перегрузка и параметры по умолчанию

Функция с именем, начинающимся с заглавной буквы, экспортируется из пакета. Например, ParsePort доступна коду других пакетов через имя пакета. Функция parsePort остаётся внутри своего пакета.

В Go нет перегрузки функций по набору параметров. Объявить в одном пакете две обычные функции add, одну для int, другую для float64, нельзя. Используйте разные имена. Для однотипных операций над разными типами иногда подходят обобщённые функции.

Параметров со значениями по умолчанию тоже нет. Для частого сценария можно написать отдельную функцию: она вызовет основную с готовыми настройками. Если настроек много, передайте структуру конфигурации. Так вызывающему коду не придётся запоминать смысл каждого позиционного аргумента.

Передача параметров в функцию: значения и указатели

В Go все аргументы передаются по значению: функция получает копию. Изменение числового параметра не меняет исходную переменную:

func increment(value int) {
	value++
}

// Внутри main:
number := 10
increment(number)
fmt.Println(number) // 10

value++ увеличивает локальную копию. Значение number остаётся равным 10.

Чтобы изменить исходное число, можно передать указатель:

func incrementAt(value *int) {
	(*value)++
}

// Внутри main:
number := 10
incrementAt(&number)
fmt.Println(number) // 11

Указатель тоже копируется. Но копия указывает на ту же переменную, поэтому запись через *value меняет number. Скобки в (*value)++ помогают прочитать выражение: сначала обращаемся к значению по указателю, затем увеличиваем его.

В incrementAt нужно передать указатель на число. Вызов incrementAt(nil) приведёт к панике при разыменовании. Если ваша функция должна принимать nil, добавьте проверку до обращения к *value.

Для вычисления нового числа можно обойтись без указателя: number = add(number, 1). Присваивание показывает, какая переменная изменится. Указатель выбирайте, когда функция должна изменить переданный объект.

Почему срез и map могут измениться

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

func replaceFirst(values []int) {
	if len(values) > 0 {
		values[0] = 99
	}
}

// Внутри main:
numbers := []int{1, 2, 3}
replaceFirst(numbers)
fmt.Println(numbers) // [99 2 3]

Запись в values[0] меняет элемент общего массива. При этом длина среза вызывающего кода сама не меняется. Если функция делает append, новый срез обычно нужно вернуть и присвоить снаружи:

func withValue(values []int, value int) []int {
	return append(values, value)
}

// Внутри main:
numbers := []int{1, 2}
numbers = withValue(numbers, 3)
fmt.Println(numbers) // [1 2 3]

append может использовать прежний массив или выделить новый. Возвращаемое значение содержит актуальную длину и ссылку на нужный массив. Поэтому нельзя рассчитывать, что локальное присваивание values = append(values, value) обновит переменную среза у вызывающего кода.

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

Как вернуть несколько значений из функции

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

func divide(a, b int) (int, int) {
	return a / b, a % b
}

// Внутри main:
quotient, remainder := divide(17, 5)
fmt.Println(quotient, remainder) // 3 2

Первый результат содержит частное, второй остаток от целочисленного деления. Делитель b должен быть ненулевым. Если он поступает из пользовательского ввода, проверьте его до деления или верните ошибку.

При присваивании число принимающих переменных должно совпадать с числом результатов. Если нужен только первый результат, второй можно отбросить через _:

quotient, _ := divide(17, 5)
fmt.Println(quotient) // 3

Через _ отбрасывайте результат, который не влияет на дальнейшую работу. Если отбросить error, программа продолжит выполнение даже после неудачного вызова.

Несколько результатов не образуют значение кортежа, которое можно сохранить в одну переменную. Запись result := divide(17, 5) не скомпилируется. Если результаты нужно хранить и передавать как единый объект, для этого подходит структура.

Возврат результата и ошибки: (T, error)

Функция, которая может завершиться ошибкой, часто возвращает (T, error): результат типа T и ошибку. При успехе ошибка равна nil. Вызывающий код сначала проверяет её, затем использует результат.

Разберём преобразование строки в номер порта:

func parsePort(text string) (int, error) {
	port, err := strconv.Atoi(text)
	if err != nil {
		return 0, fmt.Errorf("неверный порт %q: %w", text, err)
	}
	if port < 1 || port > 65535 {
		return 0, fmt.Errorf("порт вне диапазона 1..65535: %d", port)
	}
	return port, nil
}

// Внутри main:
port, err := parsePort("8080")
if err != nil {
	fmt.Println(err)
	return
}
fmt.Println(port) // 8080

strconv.Atoi возвращает число и ошибку преобразования. Наша функция добавляет контекст через fmt.Errorf с %w. Затем проверяет диапазон от 1 до 65535, выбранный для этого приложения. В других задачах порт 0 допустим: например, так можно попросить ОС выбрать свободный порт.

При ошибке parsePort возвращает 0. Использовать его как номер порта нельзя: сначала проверяем err.

Некоторые API возвращают частичные данные вместе с ошибкой. Проверяйте документацию: она определяет, можно ли использовать результат при ненулевом err. Подробнее о проверках и оборачивании ошибок написано в статье про обработку ошибок в Go.

Именованные возвращаемые значения и пустой return

Результатам можно дать имена:

func splitSeconds(total int) (minutes int, seconds int) {
	minutes = total / 60
	seconds = total % 60
	return
}

minutes и seconds являются переменными функции. При входе они получают нулевые значения, здесь 0. Пустой return возвращает их текущие значения. Такой возврат называют naked return.

Запись (minutes int, seconds int) объясняет смысл обоих чисел без чтения тела. В записи (int, int) этой подсказки нет.

При этом именованные результаты не обязывают использовать пустой return. Можно написать return total / 60, total % 60. В длинной функции явный возврат обычно легче проверить: значения видны в точке выхода. Есть и взаимодействие с отложенными вызовами: defer может изменить именованный результат перед завершением функции. Этот случай разобран отдельно в статье про defer.

Переменное число аргументов: вариативные функции

Последний параметр можно объявить как ...T. Такая функция принимает произвольное число аргументов типа T:

func sum(values ...int) int {
	total := 0
	for _, value := range values {
		total += value
	}
	return total
}

// Внутри main:
fmt.Println(sum())        // 0
fmt.Println(sum(1, 2, 3)) // 6

numbers := []int{4, 5, 6}
fmt.Println(sum(numbers...)) // 15

Внутри sum параметр values имеет тип []int. При вызове без аргументов он равен nil. Цикл по такому срезу не делает ни одной итерации, поэтому результат равен нулю.

Снаружи ... после среза передаёт его элементы в вариативный параметр. Вызов sum(numbers) не подходит: он передаёт один []int, тогда как функция ожидает аргументы типа int или раскрытый срез.

Обычные параметры разрешено ставить перед вариативным: func logValues(prefix string, values ...int). После ...int других параметров быть не должно. В одном вызове нельзя смешать отдельные вариативные аргументы с раскрытием среза: sum(1, numbers...) не скомпилируется. Если нужен такой набор, сначала соберите общий срез.

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

Анонимные функции и передача функции как аргумента

Функцию можно сохранить в переменную:

operation := add
fmt.Println(operation(4, 5)) // 9

Тип operation в этом примере равен func(int, int) int. Имена параметров не являются частью типа функции. Важны типы параметров и результатов, их порядок и наличие вариативного параметра.

Чтобы передать функцию как аргумент, укажите функциональный тип параметра. Здесь apply func(int) int принимает число и возвращает число:

func transform(values []int, apply func(int) int) []int {
	result := make([]int, len(values))
	for i, value := range values {
		result[i] = apply(value)
	}
	return result
}

// Внутри main:
doubled := transform([]int{1, 2, 3}, func(value int) int {
	return value * 2
})
fmt.Println(doubled) // [2 4 6]

Анонимная функция объявляется без имени: func(value int) int { ... }. В примере она передаётся в transform, которая вызывает её для каждого элемента. В спецификации такую запись называют функциональным литералом.

Переданную функцию часто называют callback, или функцией обратного вызова. Замените return value * 2 на return value * value, и transform вернёт квадраты чисел. Код обхода среза останется прежним.

У переменной функционального типа может быть значение nil:

var apply func(int) int
fmt.Println(apply == nil) // true

Вызов apply(1) в таком состоянии приведёт к панике. Если функция принимает callback, решите, является ли nil допустимым значением. В transform выше предполагается, что вызывающий код передал функцию.

Сравнивать функции друг с другом через == нельзя. Разрешено сравнение с nil. Если нужно выбрать поведение по имени или идентификатору, лучше хранить этот идентификатор отдельно, а не пытаться сравнить реализации.

Замыкания в Go: как сохранить состояние между вызовами

Замыкание сохраняет доступ к переменным внешней функции. Эти переменные доступны и после её завершения. Например, newCounter возвращает функцию, которая увеличивает счётчик при каждом вызове:

func newCounter(start int) func() int {
	current := start
	return func() int {
		current++
		return current
	}
}

// Внутри main:
next := newCounter(10)
fmt.Println(next()) // 11
fmt.Println(next()) // 12

other := newCounter(100)
fmt.Println(other()) // 101
fmt.Println(next())  // 13

Вызов newCounter(10) создаёт переменную current и возвращает функцию, которая её использует. После выхода из newCounter переменная остаётся доступной этой функции. Каждый вызов next увеличивает одно и то же значение.

Вызов newCounter(100) создаёт другую переменную current. Счётчики other и next работают независимо.

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

prefix := "dev"
name := func(service string) string {
	return prefix + ":" + service
}

fmt.Println(name("api")) // dev:api
prefix = "prod"
fmt.Println(name("api")) // prod:api

Копирование самого функционального значения тоже не создаёт новое состояние замыкания. После same := next вызовы same() и next() продолжат увеличивать общий счётчик. Для независимого счётчика нужен новый вызов newCounter.

Go сохраняет current, пока переменная доступна замыканию. Вручную управлять её временем жизни не нужно. Размещение на стеке или в куче определяет компилятор. Проверить его решение можно по инструкции в статье про escape analysis.

Возврат функции из функции: проверка длины строки

Создадим функцию проверки длины. Порог зададим один раз при вызове longerThan, затем будем передавать только строки:

func longerThan(limit int) func(string) bool {
	return func(value string) bool {
		return len(value) > limit
	}
}

// Внутри main:
isLong := longerThan(5)
fmt.Println(isLong("Go"))      // false
fmt.Println(isLong("Golang!")) // true

Возвращённая функция помнит limit. Здесь len измеряет длину строки в байтах. Для задачи с количеством символов такая проверка может не подойти. В примере используются строки из ASCII, поэтому различие не влияет на результат.

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

Общее состояние и горутины

Счётчик newCounter выше рассчитан на последовательные вызовы. Одновременные вызовы из нескольких горутин меняют одну переменную current без синхронизации. Замыкание не защищает её от гонки данных.

Чтобы вызывать счётчик параллельно, защитите current мьютексом или перепишите увеличение на подходящую атомарную операцию. Ещё один вариант: поручить изменение одной горутине, а остальным передавать ей запросы через канал.

FAQ: частые вопросы о функциях в Go

Как вернуть два значения из функции?
Укажите оба типа результата в скобках, например func f() (int, error). В return перечислите значения через запятую. При вызове примите их в две переменные: value, err := f().
Как передать функцию в другую функцию?
Объявите параметр функционального типа, например apply func(int) int. Передайте имя функции без скобок или анонимную функцию. Скобки нужны для вызова, а не для передачи самой функции.
Как вызвать функцию, сохранённую в переменной?
Укажите аргументы в скобках после имени переменной: operation(2, 3). Перед вызовом убедитесь, что переменная содержит функцию: вызов значения nil приведёт к панике.
Почему изменение числа внутри функции не видно снаружи?
Функция получает копию числа. Изменение параметра не меняет исходную переменную. Верните новое значение и присвойте его снаружи или передайте указатель на исходную переменную.
Почему изменение элемента среза видно снаружи?
При передаче среза копируется его описание, а базовый массив остаётся общим. Запись в элемент через параметр меняет этот массив. Если нужна независимая обработка, сначала создайте копию элементов.
Как передать срез в функцию с параметром …int?
Добавьте многоточие после среза: sum(numbers...). Вызов sum(numbers) не скомпилируется: он передаёт один срез вместо аргументов типа int.
Почему замыкание видит новое значение внешней переменной?
Замыкание сохраняет доступ к переменной. Если её значение изменится, следующий вызов увидит это изменение. Копирование функционального значения через same := next тоже сохраняет общее состояние. Для независимого счётчика вызовите newCounter ещё раз.
Что проверить, если вызов функции не компилируется?
Сравните сигнатуру и вызов: число аргументов, их типы и число переменных для результатов. При передаче среза в вариативный параметр проверьте многоточие после имени среза.

Итог

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

Несколько результатов позволяют вернуть связанные значения или результат вместе с ошибкой. Функциональные значения помогают передавать поведение, а замыкания сохраняют доступ к внешним переменным. При чтении такого кода полезно задать два вопроса: где создаётся состояние и какие вызовы его разделяют.

Для сверки синтаксиса используйте спецификацию Go: разделы Function declarations, Calls, Return statements и Function literals. Поведение библиотечных функций проверяется отдельно: strconv.Atoi и fmt.Errorf.


Об авторе: Александр Бруяко — руководитель разработки с опытом в backend, инфраструктуре и управлении командами.


Теги: