GraphLMS

Основы
Начать
Глава 04 · Основы~13 мин чтения

Функции

О чём глава

Функция — это именованный кусок логики, который принимает входные данные, что-то делает и (обычно) возвращает результат. Звучит банально, но в Go у функций есть несколько черт, которые сразу отличают язык от Python или Java: несколько возвращаемых значений, идиома value, err, переменное число аргументов и тот факт, что функция — это полноценное значение, которое можно положить в переменную, передать в аргумент и вернуть из другой функции.

В этой главе разберём всё по порядку: от обычного объявления до замыканий, которые «помнят» своё состояние. Это фундамент — почти весь Go про то, как устроены функции и как они друг другу передаются.

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

Базовая форма такая:

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

Читается слева направо: ключевое слово func, имя, список параметров в скобках, затем тип результата, затем тело. Тип параметра пишется после имени — это общее правило Go (a int, а не int a).

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

func add(a, b int) int { // a и b оба int
    return a + b
}

Функция без результата просто не указывает тип после скобок:

func greet(name string) {
    fmt.Println("Привет,", name)
}

Аргументы в Go всегда передаются по значению — функция получает копию. Если передать число или строку и изменить параметр внутри, снаружи ничего не изменится. (Для срезов, карт и указателей копируется сам «дескриптор» или адрес, поэтому через них можно менять данные — но это тема других глав.)

package main
 
import "fmt"
 
func double(x int) int {
	return x * 2
}
 
func main() {
	n := 21
	fmt.Println("вход:", n)
	fmt.Println("результат:", double(n))
	fmt.Println("n не изменился:", n)
}

Вывод:

вход: 21
результат: 42
n не изменился: 21

Множественный возврат и идиома value, err

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

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

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

q, r := divmod(17, 5) // q == 3, r == 2

Самое частое применение — пара «результат и ошибка». В Go нет исключений: вместо того чтобы «бросать» ошибку, функция возвращает её последним значением. Если всё хорошо — ошибка nil. Это и есть идиома value, err, которую вы будете видеть буквально везде:

package main
 
import (
	"errors"
	"fmt"
)
 
func safeDiv(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("деление на ноль")
	}
	return a / b, nil
}
 
func main() {
	if res, err := safeDiv(10, 2); err == nil {
		fmt.Println("10 / 2 =", res)
	}
 
	if _, err := safeDiv(10, 0); err != nil {
		fmt.Println("ошибка:", err)
	}
}

Вывод:

10 / 2 = 5
ошибка: деление на ноль

Обратите внимание на два приёма. Первый — if res, err := ...; err == nil: объявляем переменные прямо в if и тут же проверяем ошибку, область видимости ограничена телом if. Второй — _ (пустой идентификатор): так мы явно говорим «это значение мне не нужно». Игнорировать возвращённую ошибку молча — плохая привычка; либо обрабатывайте её, либо осознанно отбрасывайте через _.

Именованные возвраты

Результатам можно дать имена прямо в сигнатуре. Тогда они работают как заранее объявленные переменные, а return без аргументов («голый return») вернёт их текущие значения.

func parseRange(total, parts int) (size, remainder int) {
    size = total / parts
    remainder = total % parts
    return // вернёт size и remainder
}

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

Но злоупотреблять не стоит. В длинной функции «голый return» заставляет читателя прокручивать код вверх, чтобы понять, что же вернулось. Поэтому практическое правило простое: именованные возвраты — для коротких функций, где всё видно с одного экрана. В длинных пишите значения в return явно.

package main
 
import "fmt"
 
// minMax возвращает минимум и максимум из трёх чисел.
func minMax(a, b, c int) (lo, hi int) {
	lo, hi = a, a
	for _, v := range []int{b, c} {
		if v < lo {
			lo = v
		}
		if v > hi {
			hi = v
		}
	}
	return // голый return: вернёт lo и hi
}
 
func main() {
	lo, hi := minMax(7, 2, 9)
	fmt.Println("min =", lo, "max =", hi)
}

Вывод:

min = 2 max = 9

Variadic: переменное число аргументов

Иногда заранее неизвестно, сколько аргументов придёт. Тогда последний параметр объявляют с многоточием — ...T. Внутри функции он виден как обычный срез []T, а вызвать можно с любым числом аргументов, включая ноль.

package main
 
import "fmt"
 
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}
 
func main() {
	fmt.Println(sum(1, 2, 3))    // 6
	fmt.Println(sum(10, 20))     // 30
	fmt.Println(sum())           // 0 — ноль аргументов это нормально
}

Вывод:

6
30
0

А если у вас уже есть готовый срез и его нужно «разложить» в variadic-функцию? Для этого при вызове ставят ... после среза — это раскрывает его в отдельные аргументы:

nums := []int{4, 5, 6}
fmt.Println(sum(nums...)) // 15, эквивалент sum(4, 5, 6)

Кстати, знакомый fmt.Println — тоже variadic-функция: её сигнатура принимает ...interface{}, поэтому ей можно передать сколько угодно значений любых типов.

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

В Go функция — это значение первого класса. Её можно присвоить переменной, у которой будет тип-функция, и потом вызвать через эту переменную:

package main
 
import "fmt"
 
func main() {
	var op func(int, int) int // переменная типа «функция (int,int) int»
 
	op = func(a, b int) int { return a + b }
	fmt.Println("сумма:", op(3, 4))
 
	op = func(a, b int) int { return a * b }
	fmt.Println("произведение:", op(3, 4))
}

Вывод:

сумма: 7
произведение: 12

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

package main
 
import "fmt"
 
// apply применяет переданную функцию ко всем элементам и печатает результат.
func apply(nums []int, f func(int) int) {
	for _, n := range nums {
		fmt.Print(f(n), " ")
	}
	fmt.Println()
}
 
func main() {
	data := []int{1, 2, 3, 4}
	apply(data, func(x int) int { return x * x }) // квадраты
	apply(data, func(x int) int { return x + 10 }) // +10
}

Вывод:

1 4 9 16 
11 12 13 14 

Замыкания

Замыкание (closure) — это функция, объявленная внутри другой функции, которая «захватывает» переменные из окружающего контекста и продолжает иметь к ним доступ даже после того, как внешняя функция завершилась. Захватываются не копии значений, а сами переменные — поэтому замыкание может их менять, и эти изменения сохраняются между вызовами.

Классический пример — счётчик. Внешняя функция объявляет count и возвращает маленькую функцию, которая при каждом вызове увеличивает его:

package main
 
import "fmt"
 
func makeCounter() func() int {
	count := 0
	return func() int {
		count++ // меняем переменную из внешней области
		return count
	}
}
 
func main() {
	next := makeCounter()
	fmt.Println(next()) // 1
	fmt.Println(next()) // 2
	fmt.Println(next()) // 3
 
	other := makeCounter()
	fmt.Println(other()) // 1 — у каждого счётчика своё состояние
}

Вывод:

1
2
3
1

Ключевой момент в последних строках: каждый вызов makeCounter() создаёт новую переменную count и новое замыкание над ней. Поэтому next и other независимы — у каждого свой счётчик. Это и делает замыкания удобным способом «упаковать» состояние без объявления отдельной структуры.

Частые ошибки

  • Игнорирование ошибки. res, _ := safeDiv(a, b) молча выбрасывает ошибку, и если она была — res содержит мусорное нулевое значение. Отбрасывайте ошибку через _ только когда точно уверены, что она здесь невозможна.
  • «Голый return» в длинной функции. Технически работает, но читать тяжело: непонятно, что вернулось. Именованные возвраты — для коротких функций.
  • Захват переменной цикла в замыкании (до Go 1.22). Если внутри for создавать замыкания, они захватывают одну и ту же переменную цикла. В старых версиях Go все замыкания видели её финальное значение. Начиная с Go 1.22 каждая итерация получает свежую переменную, и проблема ушла — но в чужом коде вы ещё встретите обходной приём v := v внутри цикла.
  • Путаница ... в объявлении и при вызове. В сигнатуре nums ...int делает параметр variadic. При вызове sum(s...) раскрывает готовый срез s в аргументы. Это две разные роли одного синтаксиса.

Что дальше

Вы научились объявлять функции, возвращать несколько значений, работать с идиомой value, err, variadic-параметрами и замыканиями. Дальше функции будут встречаться постоянно: как методы у типов, как обработчики, как горутины.

Идиома value, err — это только начало разговора об ошибках; полноценная их обработка (оборачивание, errors.Is, собственные типы ошибок) — отдельная большая тема, ошибки как значения. А замыкания, запущенные через go, — прямой мостик к горутинам, где захват переменных из контекста становится особенно важным (и опасным) приёмом.