Урок 7. Функции, несколько значений и ошибки
Функция — именованный кусок кода, который можно вызывать много раз с разными данными. В 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
Привет, Анна- После
func— имя, в скобках — параметры с типами, после скобок — тип результата. - Если у соседних параметров один тип, его пишут один раз:
width, height int. - Функция без результата (
greet) типа после скобок не имеет. returnвозвращает значение и сразу завершает функцию.
Порядок функций в файле не важен: main может стоять и в начале, и в конце.
Несколько результатов
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Типы результатов перечисляют в скобках. При вызове значения раскладываются по переменным; ненужное отбрасывают через _.
Переменное число аргументов
Параметр с ... перед типом принимает сколько угодно значений, внутри функции это срез:
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.
Функции как значения
Функцию можно положить в переменную, передать в другую функцию или вернуть. Функция, объявленная внутри другой, видит её переменные — это замыкание:
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. Такие функции пригодятся для сортировки (Как отсортировать срез) и в горутинах.
Ошибки: тип error
Исключений, как в Java или Python, в Go нет. Функция, которая может не справиться, возвращает последним результатом значение типа error. Если всё хорошо — это nil (пусто), если нет — описание ошибки. Вызывающий код проверяет её сразу после вызова:
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
ошибка: деление на нольerrors.New("текст")создаёт ошибку с текстом.- При ошибке остальные результаты обычно нулевые (
0, err), и пользоваться ими не принято. if err != nil { … }— самая частая строка в программах на Go. Ошибку не прячут: либо обрабатывают, либо возвращают выше.- Текст ошибки принято писать с маленькой буквы и без точки в конце: его часто вставляют в другие сообщения.
Ошибка с подробностями: fmt.Errorf
fmt.Errorf собирает текст ошибки по формату, как Printf. Глагол %w оборачивает исходную ошибку — её текст попадает в новый, а сама она остаётся доступной для проверки:
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:
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) проверяет, что файла нет.
panic — не для обычных ошибок
panic аварийно останавливает программу и печатает, где это случилось. Сам Go вызывает панику при делении целого на ноль, выходе за границы среза, записи в nil-map. В своём коде panic оставляют для ситуаций «так не может быть никогда», а всё, что может случиться на практике (нет файла, неверный ввод, сеть недоступна), возвращают как error.
Частые ошибки
- Результат с ошибкой проигнорирован:
n, _ := strconv.Atoi(s)— при неверной строкеnмолча будет 0. missing return— у функции есть тип результата, но не на всех путях стоитreturn.too many return valuesилиnot enough return values—returnвозвращает не столько значений, сколько объявлено.assignment mismatch: 1 variable but divide returns 2 values— результат функции с двумя значениями положили в одну переменную. Нужноq, err := divide(…).
Коротко
func имя(a, b int) тип { … }; несколько результатов —(int, error).- Переменное число аргументов —
...int, передать срез —f(s...). - Функция — значение: её можно хранить в переменной и возвращать; замыкание видит внешние переменные.
- Ошибка — последний результат типа
error;nilзначит «ошибки нет». errors.New,fmt.Errorfс%wиerrors.Is— создать, обернуть и узнать ошибку.panic— только для невозможных ситуаций.