http.ListenAndServe("127.0.0.1:8080", mux). А когда добавили, получили ошибку listen tcp 127.0.0.1:8080: bind: address already in use — и поняли, что запущена другая программа, которая уже слушает этот порт. Может, у вас что-то подобное?nil, функция завершилась корректно. В противном случае нужно обработать ошибку и/или вернуть её выше по стеку. Если функция завершилась с ошибкой, не стоит использовать остальные возвращаемые значения: они могут быть не определены, функция может выполниться не полностью и не успеть вычислить значения.error;error:type error interface {
Error() string
} fmt.Errorf(format string, a ...interface{}), где указывают шаблон форматирования и дополнительные параметры. Ещё можно использовать функцию errors.New(text string), принимающую в параметре строку:func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid user's id")
}
// FindUser ищет в БД пользователя с указанным id
// если пользователь не найден, то user равен nil
// также может возвращаться ошибка, возникшая в процессе поиска
user, err := FindUser(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, fmt.Errorf("can't find user (id: %d)", id)
}
return user, nil
} Error() string и возвращать ошибки с дополнительной информацией.// TimeError предназначен для ошибок с фиксацией времени возникновения.
type TimeError struct {
Time time.Time
Err error
}
// Error добавляет поддержку интерфейса error для типа TimeError.
func (te *TimeError) Error() string {
return fmt.Sprintf("%v %v", te.Time.Format("2006/01/02 15:04:05"), te.Err)
}
// NewTimeError записывает ошибку err в тип TimeError c текущим временем.
func NewTimeError(err error) error {
return &TimeError{
Time: time.Now(),
Err: err,
}
} errors:type errorString struct {
s string
}
func New(text string) error {
return &errorString{text}
}
func (e *errorString) Error() string {
return e.s
} New() и метод Error() таким образом:func New(text string) error {
return errorString{text}
}
func (e errorString) Error() string {
return e.s
} errors.New("EOF") была бы равна ошибке io.EOF, которая определяется точно так же в пакете io, что могло бы привести к неверной обработке ошибок.TimeError. Допустим, при старте программы нужно прочитать файл конфигурации, а если не удалось его прочитать, то следует вывести ошибку и завершить работу:func ReadTextFile(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return ``, NewTimeError(err)
}
return string(data), nil
}
func main() {
data, err := ReadTextFile("myconfig.yaml")
if err != nil {
fmt.Println(err)
os.Exit(0)
}
// ...
} 2023/01/24 23:00:00 open myconfig.yaml: no such file or directory Errorf() добавить к ошибке спецификатор %w. errors.Unwrap() снимает один уровень обёртки. Если ошибка была обёрнута несколько раз, то для получения исходной ошибки нужно вызывать errors.Unwrap() до тех пор, пока она не начнёт возвращать nil.package main
import (
"errors"
"fmt"
"os"
"time"
)
func ReadTextFile(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
// добавляем время и обёртываем ошибку
now := time.Now().Format("2006/01/02 15:04:05")
return "", fmt.Errorf("%s %w", now, err)
}
return string(data), nil
}
func main() {
data, err := ReadTextFile("myconfig.yaml")
if err != nil {
fmt.Println(err)
// можем узнать оригинальную ошибку
fmt.Println("Original error:", errors.Unwrap(err))
os.Exit(0)
}
fmt.Println(data)
// ...
} Unwrap() и выводится в консоль. Unwrap() или упакованная ошибка отсутствует, то errors.Unwrap() вернёт nil. Поэтому следует определять метод Unwrap() для типов ошибок, которые упаковывают исходные ошибки.TimeError. Чтобы иметь возможность получать исходную ошибку, добавим для TimeError метод Unwrap():// остальные функции и методы остались без изменений
// ...
func (te *TimeError) Unwrap() error {
return te.Err
}
func ReadTextFile(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return ``, NewTimeError(err)
}
return string(data), nil
}
func main() {
data, err := ReadTextFile("myconfig.yaml")
if err != nil {
fmt.Println(err)
// можем узнать оригинальную ошибку для TimeError
fmt.Println("Original error:", errors.Unwrap(err))
os.Exit(0)
}
// ...
} %w или упаковывая ошибки в свои типы, можно создавать длинную последовательность вложенных ошибок. Тогда применять функцию Unwrap(), чтобы найти изначальную ошибку, становится неудобно. В пакете errors есть ещё две функции для работы с упакованными ошибками: Is() и As().Is():func Is(err, target error) bool err, и вам нужно сравнить её с ошибкой target. Многие пакеты содержат предопределённые ошибки. err ошибке io.EOF. Использовать сравнение err == io.EOF нельзя, так как err может содержать обёрнутые ошибки, включая io.EOF. Функция errors.Is() сравнивает ошибку err с target, причём ошибка target ищется среди всех вложенных ошибок. Функция возвращает true, если текущая ошибка err равна target или содержит ошибку target. os.ReadFile будет возвращать ошибку os.ErrNotExist, которая определена в пакете os. Добавим проверку этой ошибки в предыдущий пример с TimeError — в функции main потребуются совсем небольшие изменения:func main() {
data, err := ReadTextFile("myconfig.yaml")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// создаём файл
// ...
} else {
fmt.Println(err)
os.Exit(0)
}
}
// ...
} ReadTestFile возвращает ошибку типа *TimeError, функция Is() вернёт true, если начальная ошибка была os.ErrNotExist. Без функции Is() для правильной работы программы пришлось бы вызывать Unwrap() и сравнивать возвращаемую ей ошибку с os.ErrNotExist.As(). Рассмотрим ситуацию, при которой нужно привести ошибку к определённому типу:var myErr *TimeError
if myErr, ok = err.(*TimeError); ok {
// ...
} err.(*TimeError) закончится неудачей и значение ok будет false. Функция errors.As() проверяет ошибку на соответствие определённому типу и приводит её к данному типу, но при этом учитываются все обёрнутые ошибки:func As(err error, target any) bool var myErr *TimeError
if errors.As(err, &myErr) {
// ...
} err имеет такой же тип, на какой указывает target, или содержит обёрнутую ошибку такого же типа, то As() присваивает значению target эту ошибку и возвращает true. В противном случае возвращается false. Параметр target должен быть ненулевым указателем.TimeError:type TimeError struct {
Time time.Time
Err error
}
func main() {
data, err := ReadTextFile("myconfig.yaml")
if err != nil {
var te *TimeError
if errors.As(err, &te) {
fmt.Printf("ошибка %v возникла %s", te.Err,
te.Time.Format("02.01.06 15:04:05"))
}
// ...
}
// ...
} NewTimeError() возвращает указатель, а не структуру, в примере выше te — это указатель на TimeError.// экспортируемая ошибка, доступна из других пакетов
var ErrAccessDenied = errors.New("access denied")
func LoadSettings(userID int) error {
if !AdminUser(userID) {
return fmt.Errorf("%w", ErrAccessDenied)
}
// загружаем настройки
} // плохо
if err == ErrAccessDenied {
}
// хорошо
if errors.Is(err, ErrAccessDenied) {
}
var myErr *MyError
// плохо
if myErr, ok = err.(*MyError); ok {
}
// хорошо
if errors.As(err, &myErr) {
} func Join(errs ...error) error Join() будут переданы ошибки, равные nil, то они будут пропущены. Таким образом, если все параметры равны nil, функция тоже вернёт nil.Join(), можно вызывать функции Is() и As(), но Uwrap() будет возвращать nil. Join():package main
import (
"encoding/json"
"errors"
"fmt"
)
type Settings struct {
Host string
Port int
}
var (
ErrNoHost = errors.New("Не указан host")
ErrNoPort = errors.New("Не указан port")
)
func ParseSettings(input string) (*Settings, error) {
var settings Settings
err := json.Unmarshal([]byte(input), &settings)
if err != nil {
return nil, err
}
// находим сразу все ошибки
var errs []error
if len(settings.Host) == 0 {
errs = append(errs, ErrNoHost)
}
if settings.Port == 0 {
errs = append(errs, ErrNoPort)
}
return &settings, errors.Join(errs...)
}
func main() {
settings, err := ParseSettings(`{"host":"localhost", "port": 3000}`)
fmt.Println(err, settings)
_, err = ParseSettings("{}")
fmt.Println(err)
fmt.Println(errors.Is(err, ErrNoHost), errors.Is(err, ErrNoPort))
} <nil> &{localhost 3000}
Не указан host
Не указан port
true true errors содержит небольшое количество функций — Go-разработчику важно понимать назначение каждой из них и уметь использовать эти функции на практике. internal/store/store.go. Добавим в интерфейс хранилища метод RegisterUser() и особую ошибку:package store
import (
"context"
"errors"
"time"
)
// ErrConflict указывает на конфликт данных в хранилище.
var ErrConflict = errors.New("data conflict")
type Store interface {
FindRecepient(ctx context.Context, username string) (userID string, err error)
ListMessages(ctx context.Context, userID string) ([]Message, error)
GetMessage(ctx context.Context, id int64) (*Message, error)
SaveMessage(ctx context.Context, userID string, msg Message) error
// RegisterUser регистрирует нового пользователя
RegisterUser(ctx context.Context, userID, username string) error
}
... ErrConflict будем использовать во всех реализациях хранилища для оповещения о возможном нарушении целостности данных. В частности, здесь используем её для того, чтобы указать на попытку зарегистрировать нового пользователя с уже имеющимся именем.internal/store/pg/store.go новым методом RegisterUser():package pg
import (
"context"
"database/sql"
"errors"
"time"
"github.com/bluegopher/alice-skill/internal/store"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
)
// Store реализует интерфейс store.Store и позволяет взаимодействовать с СУБД PostgreSQL.
type Store struct {
// Поле conn содержит объект соединения с СУБД.
conn *sql.DB
}
...
func (s Store) RegisterUser(ctx context.Context, userID, username string) error {
// добавляем новую запись пользователя
_, err := s.conn.ExecContext(ctx, `
INSERT INTO users
(id, username)
VALUES
($1, $2);
`, userID, username)
if err != nil {
// проверяем, что ошибка сигнализирует о потенциальном нарушении целостности данных
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
err = store.ErrConflict
}
}
return err
} $ go mod tidy, чтобы внести в список зависимостей пакет github.com/jackc/pgerrcode, который отвечает за интроспекцию специфичных ошибок СУБД PostgreSQL.cmd/skill/app.go:func (a *app) webhook(w http.ResponseWriter, r *http.Request) {
...
// текст ответа навыка
var text string
switch true {
case strings.HasPrefix(req.Request.Command, "Отправь"):
...
case strings.HasPrefix(req.Request.Command, "Прочитай"):
...
// пользователь хочет зарегистрироваться
case strings.HasPrefix(req.Request.Command, "Зарегистрируй"):
// гипотетическая функция parseRegisterCommand вычленит из запроса
// желаемое имя нового пользователя
username := parseRegisterCommand(req.Request.Command)
// регистрируем пользователя
err := a.store.RegisterUser(ctx, req.Session.User.UserID, username)
// наличие неспецифичной ошибки
if err != nil && !errors.Is(err, store.ErrConflict) {
logger.Log.Debug("cannot register user", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
// определяем правильное ответное сообщение пользователю
text = fmt.Sprintf("Вы успешно зарегистрированы под именем %s", username)
if errors.Is(err, store.ErrConflict) {
// ошибка специфична для случая конфликта имён пользователей
text = "Извините, такое имя уже занято. Попробуйте другое."
}
default:
...
}
// заполняем модель ответа
resp := models.Response{
Response: models.ResponsePayload{
Text: text, // Алиса проговорит текст
},
Version: "1.0",
}
w.Header().Set("Content-Type", "application/json")
// сериализуем ответ сервера
enc := json.NewEncoder(w)
if err := enc.Encode(resp); err != nil {
logger.Log.Debug("error encoding response", zap.Error(err))
return
}
logger.Log.Debug("sending HTTP 200 response")
} errors.