Кастомные ошибки в Go
Базовые ошибки
package main
import (
"errors"
"fmt"
)
func main() {
// Простейшая ошибка
err := errors.New("something went wrong")
fmt.Println(err)
// С форматированием
err = fmt.Errorf("user %d not found", 42)
fmt.Println(err)
}
Типы ошибок
package main
import (
"errors"
"fmt"
)
// 1. Ошибка как значение
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// 2. Ошибка с кодом
type StatusError struct {
Code int
Message string
}
func (e *StatusError) Error() string {
return fmt.Sprintf("status %d: %s", e.Code, e.Message)
}
// 3. Ошибка с контекстом
type APIError struct {
Endpoint string
Status int
Err error
}
func (e *APIError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Endpoint, e.Err)
}
return fmt.Sprintf("%s: status %d", e.Endpoint, e.Status)
}
// 4. Оборачивание ошибок
func (e *APIError) Unwrap() error {
return e.Err
}
func main() {
// Использование
err := &ValidationError{Field: "email", Message: "invalid format"}
fmt.Println(err) // email: invalid format
err2 := &StatusError{Code: 404, Message: "not found"}
fmt.Println(err2) // status 404: not found
err3 := &APIError{
Endpoint: "/api/users",
Status: 500,
Err: errors.New("database connection failed"),
}
fmt.Println(err3) // /api/users: database connection failed
}
Sentinel Errors
package main
import (
"errors"
"fmt"
)
// Sentinel errors — предопределённые ошибки для проверки
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidInput = errors.New("invalid input")
ErrConnectionFailed = errors.New("connection failed")
ErrTimeout = errors.New("timeout")
)
func findUser(id int) error {
if id == 0 {
return ErrNotFound
}
if id < 0 {
return ErrPermissionDenied
}
return nil
}
func main() {
err := findUser(0)
// Проверка на конкретную ошибку
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found!")
}
// Оборачивание с добавлением контекста
if err != nil {
fmt.Errorf("failed to find user: %w", err)
}
}
Оборачивание ошибок (error wrapping)
package main
import (
"errors"
"fmt"
)
func level3() error {
return errors.New("root error")
}
func level2() error {
err := level3()
return fmt.Errorf("level 2: %w", err)
}
func level1() error {
err := level2()
return fmt.Errorf("level 1: %w", err)
}
func main() {
err := level1()
// Выводим всю цепочку
fmt.Println("Full chain:")
for {
fmt.Println("-", err)
if errors.Unwrap(err) == nil {
break
}
err = errors.Unwrap(err)
}
// errors.Is проходит по всей цепочке
if errors.Is(err, errors.New("root error")) {
fmt.Println("Found root error!")
}
}
Паттерны создания ошибок
1. Фабричные функции
package main
import (
"fmt"
)
type AppError struct {
Code string
Message string
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// Фабрики
func NewNotFoundError(resource, id string) *AppError {
return &AppError{
Code: "NOT_FOUND",
Message: fmt.Sprintf("%s %s not found", resource, id),
}
}
func NewValidationError(field, msg string) *AppError {
return &AppError{
Code: "VALIDATION_ERROR",
Message: fmt.Sprintf("%s: %s", field, msg),
}
}
func NewInternalError(msg string, cause error) *AppError {
return &AppError{
Code: "INTERNAL_ERROR",
Message: msg,
Cause: cause,
}
}
func main() {
err := NewNotFoundError("user", "123")
fmt.Println(err) // NOT_FOUND: user 123 not found
}
2. Ошибки с данными
package main
import (
"errors"
"fmt"
)
type FieldError struct {
Field string
Value interface{}
Msg string
}
func (f FieldError) Error() string {
return fmt.Sprintf("field %q: %v - %s", f.Field, f.Value, f.Msg)
}
func validateEmail(email string) error {
if email == "" {
return FieldError{Field: "email", Value: email, Msg: "required"}
}
if len(email) < 5 {
return FieldError{Field: "email", Value: email, Msg: "too short"}
}
return nil
}
func main() {
err := validateEmail("")
if err != nil {
fmt.Println(err)
// Можно привести к типу для доступа к полям
if fe, ok := err.(FieldError); ok {
fmt.Printf("Field: %s, Value: %v\n", fe.Field, fe.Value)
}
}
}
3. Ошибки с кодами HTTP
package main
import (
"errors"
"fmt"
"net/http"
)
type HTTPError struct {
Code int
Message string
}
func (e *HTTPError) Error() string {
return e.Message
}
// Интерфейс для проверки статуса
type Coder interface {
Code() int
}
func (e *HTTPError) Code() int {
return e.Code
}
func NotFound(msg string) *HTTPError {
return &HTTPError{Code: http.StatusNotFound, Message: msg}
}
func BadRequest(msg string) *HTTPError {
return &HTTPError{Code: http.StatusBadRequest, Message: msg}
}
func Internal(msg string) *HTTPError {
return &HTTPError{Code: http.StatusInternalServerError, Message: msg}
}
func main() {
err := NotFound("user not found")
fmt.Println(err) // user not found
fmt.Println(err.Code()) // 404
// Проверка через errors.Is
var coder Coder
if errors.As(err, &coder) {
fmt.Printf("HTTP status: %d\n", coder.Code())
}
}
Обработка ошибок в коде
package main
import (
"errors"
"fmt"
)
func divide(a, (float64, b float64) error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func processData(data []int) error {
if len(data) == 0 {
return errors.New("empty data")
}
return nil
}
// Агрегация ошибок
func processAll(items []int) error {
var errs []error
for i, item := range items {
if item < 0 {
errs = append(errs, fmt.Errorf("item %d: negative value %d", i, item))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func main() {
// Простая обработка
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
// Множественные ошибки (Go 1.20+)
err = processAll([]int{1, -2, 3, -4})
if err != nil {
fmt.Println("Errors:", err)
}
}
Библиотеки для ошибок
1. pkg/errors
// Устаревший стиль, теперь есть встроенный
// errors.Wrap, errors.WithMessage, errors.WithStack
2. Go 1.20+ — стандартный error grouping
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
err3 := errors.New("error 3")
// Объединение ошибок
combined := errors.Join(err1, err2, err3)
fmt.Println(combined)
// Проверка на конкретную ошибку
if errors.Is(combined, err1) {
fmt.Println("Contains err1")
}
// Получение всех ошибок
for _, e := range []error{err1, err2, err3} {
if errors.Is(combined, e) {
fmt.Println("Found:", e)
}
}
}
Лучшие практики
✅ Делайте:
// 1. Используйте sentinel errors для предсказуемых сценариев
var ErrNotFound = errors.New("not found")
// 2. Оборачивайте ошибки с %w
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// 3. Используйте errors.Is и errors.As для проверки
if errors.Is(err, ErrNotFound) {
// обработка
}
// 4. Создавайте понятные сообщения
return fmt.Errorf("user %d: %w", userID, err)
// 5. Сохраняйте стектрейс (через библиотеку или Go 1.20+)
// errors.Join сохраняет контекст
❌ Не делайте:
// 1. Не используйте строки для проверки
if err.Error() == "not found" { // ❌
// 2. Не теряйте исходную ошибку
return errors.New(err.Error()) // ❌
// 3. Не используйте panic для обычных ошибок
if err != nil {
panic(err) // ❌
}
// 4. Не скрывайте ошибки
_ = someFunc() // ❌