Глиф Учебники

Урок 7. Функции, несколько значений и ошибки

Курс «Go» · урок 7 из 10 · 4 минуты чтения

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

Объявление функции

Go
package main

import "fmt"

func area(width, height int) int {
    return width * height
}

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

func main() {
    fmt.Println(area(3, 4))
    greet("Анна")
}
Вывод
12
Привет, Анна

Порядок функций в файле не важен: main может стоять и в начале, и в конце.

Несколько результатов

Go
package main

import "fmt"

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

func main() {
    q, r := divmod(17, 5)
    fmt.Println(q, r)

    _, rest := divmod(10, 3)
    fmt.Println(rest)
}
Вывод
3 2
1

Типы результатов перечисляют в скобках. При вызове значения раскладываются по переменным; ненужное отбрасывают через _.

Переменное число аргументов

Параметр с ... перед типом принимает сколько угодно значений, внутри функции это срез:

Go
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))
    fmt.Println(sum())
    values := []int{10, 20, 30}
    fmt.Println(sum(values...))
}
Вывод
6
0
60

Готовый срез передают с ... после имени: sum(values...). Так устроены и fmt.Println, и append.

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

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

Go
package main

import "fmt"

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    next := counter()
    fmt.Println(next(), next(), next())

    double := func(x int) int { return x * 2 }
    fmt.Println(double(21))
}
Вывод
1 2 3
42

Каждый вызов next() увеличивает одну и ту же n, которая живёт, пока жива функция next. Такие функции пригодятся для сортировки (Как отсортировать срез) и в горутинах.

next := counter() next func() int видит n 0 n создана в counter() и живёт, пока жива функция next next() вызов 1 n++ 0 1 return n 1 next() вызов 2 n++ 1 2 return n 2 next() вызов 3 n++ 2 3 return n 3
counter() создаёт переменную n и возвращает функцию, которая её видит: n остаётся жить вместе с next, и каждый вызов next() увеличивает ту же n и возвращает 1, 2, 3.

Ошибки: тип error

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

Go
package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("деление на ноль")
    }
    return a / b, nil
}

func main() {
    q, err := divide(10, 3)
    if err != nil {
        fmt.Println("ошибка:", err)
        return
    }
    fmt.Println("частное:", q)

    _, err = divide(1, 0)
    if err != nil {
        fmt.Println("ошибка:", err)
    }
}
Вывод
частное: 3
ошибка: деление на ноль
q, err := divide(10, 3) int error 3 nil err != nil нет частное: 3 _, err = divide(1, 0) int error 0 деление на ноль err != nil да ошибка: деление на ноль
divide возвращает два значения — число и ошибку: при делении 10 на 3 ошибка равна nil и печатается частное, а при делении на ноль err != nil и печатается текст ошибки.

Ошибка с подробностями: fmt.Errorf

fmt.Errorf собирает текст ошибки по формату, как Printf. Глагол %w оборачивает исходную ошибку — её текст попадает в новый, а сама она остаётся доступной для проверки:

Go
package main

import (
    "fmt"
    "strconv"
)

func parseAge(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("возраст %q: %w", s, err)
    }
    if n < 0 || n > 150 {
        return 0, fmt.Errorf("возраст %d вне диапазона", n)
    }
    return n, nil
}

func main() {
    for _, s := range []string{"42", "abc", "200"} {
        age, err := parseAge(s)
        if err != nil {
            fmt.Println("ошибка:", err)
            continue
        }
        fmt.Println("возраст:", age)
    }
}
Вывод
возраст: 42
ошибка: возраст "abc": strconv.Atoi: parsing "abc": invalid syntax
ошибка: возраст 200 вне диапазона

По тексту сразу видно, где и на каком значении случилась ошибка. О strconv.Atoi — в справке Как перевести строку в число.

Проверка вида ошибки: errors.Is

Чтобы программа могла отличить одну ошибку от другой, заранее объявляют ошибку-образец и сверяют с ней через errors.Is — он находит образец и внутри обёрток %w:

Go
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("не найдено")

func find(prices map[string]int, name string) (int, error) {
    p, ok := prices[name]
    if !ok {
        return 0, fmt.Errorf("товар %q: %w", name, ErrNotFound)
    }
    return p, nil
}

func main() {
    prices := map[string]int{"хлеб": 2}
    _, err := find(prices, "сыр")
    fmt.Println(err)
    fmt.Println(errors.Is(err, ErrNotFound))
}
Вывод
товар "сыр": не найдено
true

Так устроены ошибки стандартной библиотеки: например, errors.Is(err, os.ErrNotExist) проверяет, что файла нет.

err новая ошибка из fmt.Errorf с %w товар "сыр": ErrNotFound не найдено исходная ошибка лежит внутри новой fmt.Println(err) товар "сыр": не найдено errors.Is(err, ErrNotFound) true образец найден внутри
fmt.Errorf с %w кладёт ErrNotFound внутрь новой ошибки err: Println печатает общий текст «товар "сыр": не найдено», а errors.Is находит образец внутри и возвращает true.

panic — не для обычных ошибок

panic аварийно останавливает программу и печатает, где это случилось. Сам Go вызывает панику при делении целого на ноль, выходе за границы среза, записи в nil-map. В своём коде panic оставляют для ситуаций «так не может быть никогда», а всё, что может случиться на практике (нет файла, неверный ввод, сеть недоступна), возвращают как error.

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

Коротко

Проверьте себя

1. Как Go сообщает вызывающему коду об ошибке в функции?
2. Что выведет программа?
Go
package main

import "fmt"

func minmax(a, b int) (int, int) {
    if a < b {
        return a, b
    }
    return b, a
}

func main() {
    lo, hi := minmax(8, 3)
    fmt.Println(lo, hi)
}
3. Что значит err == nil после вызова функции?
4. Зачем в fmt.Errorf("чтение настроек: %w", err) глагол %w?