Язык Go — функции: множественные возвращаемые значения и defer
Множественные возвращаемые значения, именованные результаты и defer — три особенности функций в Go, которые меняют привычные паттерны написания кода.
Содержание
Функции в 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привязан к функции, не к блоку — в цикле это важно учитывать