Раздел 22 · Go

Функции: вызов, multi-return, замыкания

junior~55 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

Функции: вызов, multi-return, замыкания

В Go функция это первоклассный объект (можно положить в переменную, передать в другую функцию, вернуть из функции). При этом синтаксис намеренно беднее, чем в JS или Python: нет default-аргументов, нет именованных аргументов при вызове, нет перегрузки. Меньше способов сделать одно и то же, проще читать чужой код.

После этого урока ты не “запомнишь синтаксис” (он простой), а уложишь 4 идиомы: (value, error), named returns с defer, variadic, и замыкание над переменной.

1. Объявление и вызов

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

func имя(параметры) типВозврата {
    тело
}

Минимальный пример:

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

func main() {
    s := add(2, 3) // 5
}

Сахар: подряд идущие параметры одного типа можно сократить:

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

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

func greet(name string) {
    fmt.Println("hi,", name)
}

Несколько возвратов перечисляются в скобках:

func minMax(xs []int) (int, int) {
    return xs[0], xs[len(xs)-1] // упрощение, без проверки пустого
}

Вызов читается слева направо как присваивание (a, b) = f(...):

lo, hi := minMax([]int{1, 2, 3})

2. Multiple return и идиома (value, error)

Главная идиома Go: функция, которая может ошибиться, возвращает пару (результат, error), а не бросает исключение.

func ParseAge(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("parse age %q: %w", s, err)
    }
    if n < 0 {
        return 0, fmt.Errorf("negative age: %d", n)
    }
    return n, nil
}

Шаблон вызова:

age, err := ParseAge("30")
if err != nil {
    // обработка
    return err
}
// дальше работаем с age

Это многословно, и это сделано сознательно: каждый возможный сбой виден глазами в коде, не “куда-то прыгнул через try-catch”. Подробно про error и обёртывание в 11-errors-deep.

Bool-flag вместо error

Когда “не получилось” не ошибка, а просто отсутствие, идиома такая же, но второй аргумент bool, не error:

m := map[string]int{"a": 1}
v, ok := m["b"]   // 0, false
v, ok  = m["a"]   // 1, true

Это та же пара, синтаксис языка делает её первоклассной во встроенных конструкциях (map, chan, type assertion).

3. Named returns

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

func DivMod(a, b int) (q, r int, err error) {
    if b == 0 {
        err = errors.New("division by zero")
        return // q=0, r=0, err=...
    }
    q = a / b
    r = a % b
    return
}

Что меняется:

  • q, r, err уже объявлены в начале функции, со значением 0/""/nil;
  • return без аргументов возвращает их текущие значения. Это называется naked return;
  • читается местами лучше (видно “имена” возвратов), местами хуже (надо следить за состоянием).

Когда использовать:

  • В длинных функциях, не использовать. Naked return на 30-й строке, это головоломка: не очевидно что куда уходит.
  • В коротких функциях с defer, удобно: можно подменить err в defer, и это будет последнее значение.

Пример с defer:

func ReadConfig(path string) (cfg Config, err error) {
    f, err := os.Open(path)
    if err != nil {
        return
    }
    defer func() {
        if cerr := f.Close(); cerr != nil && err == nil {
            err = cerr
        }
    }()

    return parseConfig(f)
}

Здесь defer видит err через имя, может его изменить. Без named returns пришлось бы писать руками var err error и тратить место.

4. Variadic функции

Если функция принимает переменное число аргументов одного типа, последний параметр пишется с ...:

func Sum(xs ...int) int {
    s := 0
    for _, x := range xs {
        s += x
    }
    return s
}

Sum(1, 2, 3)         // 6
Sum()                // 0
Sum([]int{1,2,3}...) // 6, разворачивание слайса

Внутри функции xs это []int. Передать существующий слайс с ... означает “не собирать новый, использовать этот”.

Каноничный пример из stdlib, fmt.Println:

func Println(a ...any) (n int, err error)

То есть Println("a", 1, true) это вызов с тремя аргументами, обёрнутый в []any{"a", 1, true}.

Несколько обычных + variadic

Variadic должен быть последним:

func Logf(level string, format string, args ...any) {
    fmt.Printf("[%s] "+format+"\n", append([]any{}, args...)...)
}

Logf("INFO", "user %s logged in from %s", "vasya", "tor")

append([]any{}, args...)... это идиоматичный способ “пробросить variadic дальше”.

5. Функции как первоклассные значения

Функция в Go это полноценное значение. Можно:

  • положить в переменную,
  • передать в другую функцию,
  • вернуть из функции,
  • сравнить с nil.

Сравнение функций между собой компилятор не разрешает (f == g, ошибка), сравнить можно только с nil (проверить, что переменная вообще что-то держит).

type IntFn func(int) int

double := func(x int) int { return x * 2 } // anonymous function
fmt.Println(double(3)) // 6

apply := func(f IntFn, x int) int { return f(x) }
apply(double, 5) // 10

func(...) ... без имени, это анонимная функция (или функция-литерал). Может быть вызвана сразу:

result := func(x int) int { return x*x }(5) // 25

Это IIFE, в Go применяется редко, но в отладке и инициализации иногда удобно.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.