· 9 мин 👁 1.3k Начинающий

Язык Go — функции: множественные возвращаемые значения и defer

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

функцииdeferименованные результатыошибки
Содержание

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

Множественные возвращаемые значения

В C, если функция сигнализирует об ошибке, её обычно кодируют в само возвращаемое значение: -1 означает ошибку, остальное — результат. Реальный код ошибки при этом уходит в глобальную переменную errno.

В Go функция возвращает и результат, и ошибку одновременно:

// Write возвращает количество записанных байт и ошибку.
// Если n != len(b), err будет не nil.
func (file *File) Write(b []byte) (n int, err error)

Это явный контракт: два результата, оба видны в сигнатуре. Никаких глобальных переменных, никакого «проверьте errno после вызова».

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

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ { // пропускаем не-цифры
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ { // читаем цифры
        x = x*10 + int(b[i]) - '0'
    }
    return x, i // число и новая позиция
}

Использование:

for i := 0; i < len(b); {
    x, i = nextInt(b, i) // i обновляется прямо здесь, без указателя
    fmt.Println(x)
}

Именованные результаты

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

Имена — это документация. Сравните:

// что из двух int — число, что — позиция?
func nextInt(b []byte, i int) (int, int)

// теперь очевидно:
func nextInt(b []byte, pos int) (value, nextPos int) 

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

// ReadFull читает из r ровно len(buf) байт.
// n — сколько прочитано, err — ошибка если не дочитали до конца.
func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr        // накапливаем количество прочитанных байт
        buf = buf[nr:] // сдвигаем буфер
    }
    return // возвращает n и err в их текущем состоянии
}

return без аргументов называют «голым» (naked return). Стоит использовать его только в коротких функциях — в длинных он снижает читаемость, так как не видно, что именно возвращается.

defer: выполни перед выходом из функции

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

Классическая задача — закрытие файла:

func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close() // будет вызван при любом выходе из функции

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...)
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err // f.Close() вызовется и здесь
        }
    }
    return string(result), nil // и здесь тоже
}

У такого подхода два преимущества. Первое — f.Close() стоит рядом с f.Open(), а не в конце функции: сразу видно, что ресурс будет освобождён. Второе — невозможно случайно забыть закрыть файл при добавлении нового раннего return.

Аргументы вычисляются в момент defer, не в момент выполнения

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

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i) // значение i фиксируется здесь: 0, 1, 2, 3, 4
}
// при выходе из функции напечатает: 4 3 2 1 0

Отложенные вызовы выполняются в порядке LIFO — последний зарегистрированный выполняется первым.

Практический пример: трассировка входа и выхода

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

func trace(s string) string {
    fmt.Println("entering:", s)
    return s // возвращает имя функции — оно понадобится для un()
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a")) // trace("a") выполняется сразу, un — при выходе
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

Вывод:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

trace("a") вызывается сразу при регистрации defer — поэтому entering печатается при входе. un — при выходе. Одна строка покрывает оба события.

Ловушка: defer в цикле

defer привязан к функции, а не к блоку. Если открывать файлы в цикле и ставить defer f.Close() внутри него, файлы закроются только при выходе из всей функции, а не после каждой итерации:

// Проблема: все файлы закроются только при выходе из processFiles
func processFiles(names []string) {
    for _, name := range names {
        f, _ := os.Open(name)
        defer f.Close() // не после итерации, а после всей функции
        process(f)
    }
}

// Решение: вынести логику в отдельную функцию
func processFiles(names []string) {
    for _, name := range names {
        processOne(name)
    }
}

func processOne(name string) {
    f, _ := os.Open(name)
    defer f.Close() // теперь закрывается после каждого вызова processOne
    process(f)
}

Итоги

  • Множественные возвращаемые значения убирают необходимость кодировать ошибки в возвращаемое значение или передавать указатели как выходные параметры
  • Именованные результаты — это документация и удобство для функций, где значения накапливаются постепенно
  • naked return допустим только в коротких функциях
  • defer гарантирует освобождение ресурсов при любом пути выхода из функции
  • Аргументы defer вычисляются немедленно — в момент вызова defer, а не выполнения
  • Отложенные вызовы выполняются в порядке LIFO
  • defer привязан к функции, не к блоку — в цикле это важно учитывать