Обработка ошибок
Обработка ошибок — одна из самых характерных особенностей Go. Вместо исключений (exceptions) Go использует явный возврат ошибок как значений.
"Errors are values." — Rob Pike
Интерфейс error
В Go ошибка — это любое значение, реализующее интерфейс error:
// Built-in error interface
type error interface {
Error() string
}
Это один из самых маленьких и при этом самых важных интерфейсов в Go.
Базовая обработка ошибок
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
// The fundamental Go error handling pattern
f, err := os.Open("config.json")
if err != nil {
fmt.Println("Error:", err)
return
}
defer f.Close()
// Type conversion with error
n, err := strconv.Atoi("not-a-number")
if err != nil {
fmt.Println("Conversion error:", err)
// Handle error, don't just log and continue
}
_ = n
}
Идиома Go: Проверяйте ошибку сразу после вызова. Счастливый путь (happy path) должен идти с минимальным отступом.
// GOOD: happy path with minimal indentation
func process() error {
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch data: %w", err)
}
result, err := transform(data)
if err != nil {
return fmt.Errorf("transform: %w", err)
}
if err := save(result); err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}
// BAD: deeply nested happy path
func processBad() error {
data, err := fetchData()
if err == nil {
result, err := transform(data)
if err == nil {
err := save(result)
if err == nil {
return nil
}
return err
}
return err
}
return err
}
Создание ошибок
errors.New — простая ошибка
package main
import (
"errors"
"fmt"
)
// Simple error with a static message
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
user, err := findUser(-1)
if err != nil {
fmt.Println(err) // "not found"
return
}
fmt.Println(user)
}
fmt.Errorf — форматированные ошибки
package main
import "fmt"
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("invalid age: %d (must be non-negative)", age)
}
if age > 150 {
return fmt.Errorf("invalid age: %d (unrealistic value)", age)
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err) // "invalid age: -5 (must be non-negative)"
}
}
Оборачивание ошибок (Error Wrapping)
Начиная с Go 1.13, глагол %w в fmt.Errorf позволяет оборачивать ошибки, сохраняя цепочку причин:
package main
import (
"errors"
"fmt"
"os"
)
var ErrConfigNotFound = errors.New("config not found")
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// Wrap the original error with context
return nil, fmt.Errorf("read config %s: %w", path, err)
}
return data, nil
}
func loadApp() error {
_, err := readConfig("/etc/myapp/config.yaml")
if err != nil {
// Wrap again — building an error chain
return fmt.Errorf("load app: %w", err)
}
return nil
}
func main() {
err := loadApp()
if err != nil {
fmt.Println(err)
// Output: load app: read config /etc/myapp/config.yaml:
// open /etc/myapp/config.yaml: no such file or directory
// Check if the error chain contains a specific error
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist!")
}
}
}
%w vs %v/%s
| Формат | Оборачивает? | errors.Is/As работает? | Пример |
|---|---|---|---|
%w |
Да | Да | fmt.Errorf("wrap: %w", err) |
%v |
Нет | Нет | fmt.Errorf("context: %v", err) |
%s |
Нет | Нет | fmt.Errorf("context: %s", err) |
Правило: Используйте
%w, когда вызывающий код должен иметь возможность проверить оригинальную ошибку. Используйте%v, когда хотите скрыть детали реализации.
Множественное оборачивание (Go 1.20+)
// Go 1.20+: wrap multiple errors
func validateForm(name, email string) error {
var errs []error
if name == "" {
errs = append(errs, errors.New("name is required"))
}
if email == "" {
errs = append(errs, errors.New("email is required"))
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// fmt.Errorf can wrap multiple errors too (Go 1.20+)
func process() error {
err1 := step1()
err2 := step2()
if err1 != nil || err2 != nil {
return fmt.Errorf("process failed: %w, %w", err1, err2)
}
return nil
}
errors.Is, errors.As, errors.Unwrap
errors.Is — проверка по значению
package main
import (
"errors"
"fmt"
"io"
"os"
)
var (
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
)
func findItem(id int) error {
// Wrapping ErrNotFound
return fmt.Errorf("find item %d: %w", id, ErrNotFound)
}
func main() {
err := findItem(42)
// errors.Is traverses the ENTIRE error chain
if errors.Is(err, ErrNotFound) {
fmt.Println("Item not found") // This prints!
}
// Works with standard library sentinel errors too
_, err = os.Open("nonexistent.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
}
// Check for io.EOF
if errors.Is(err, io.EOF) {
fmt.Println("End of file")
}
// Direct comparison (== ) only checks the TOP-level error
if err == ErrNotFound {
// This does NOT work for wrapped errors!
}
}
errors.As — проверка по типу
package main
import (
"errors"
"fmt"
"net"
"os"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: field %q — %s", e.Field, e.Message)
}
func validateUser(name string) error {
if name == "" {
return fmt.Errorf("validate user: %w", &ValidationError{
Field: "name",
Message: "cannot be empty",
})
}
return nil
}
func main() {
err := validateUser("")
// errors.As extracts the error of a specific TYPE from the chain
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
// Field: name, Message: cannot be empty
}
// Works with standard library error types
_, err = net.Dial("tcp", "invalid:address")
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Printf("Network error: op=%s, net=%s\n", netErr.Op, netErr.Net)
}
// os.PathError example
_, err = os.Open("nonexistent.txt")
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error: op=%s, path=%s\n", pathErr.Op, pathErr.Path)
}
}
errors.Unwrap
func main() {
inner := errors.New("database connection failed")
wrapped := fmt.Errorf("user service: %w", inner)
outer := fmt.Errorf("API handler: %w", wrapped)
// Unwrap one level at a time
fmt.Println(errors.Unwrap(outer)) // "user service: database connection failed"
fmt.Println(errors.Unwrap(wrapped)) // "database connection failed"
fmt.Println(errors.Unwrap(inner)) // nil (no wrapped error)
}
Пользовательские типы ошибок
Ошибка с дополнительными данными
package main
import (
"fmt"
"time"
)
// Custom error with HTTP status code
type HTTPError struct {
StatusCode int
Message string
URL string
Timestamp time.Time
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s (url: %s)", e.StatusCode, e.Message, e.URL)
}
// Implement Is() for custom equality comparison
func (e *HTTPError) Is(target error) bool {
t, ok := target.(*HTTPError)
if !ok {
return false
}
return e.StatusCode == t.StatusCode
}
// Predefined sentinel-like typed errors
var (
ErrHTTPNotFound = &HTTPError{StatusCode: 404, Message: "Not Found"}
ErrHTTPUnauthorized = &HTTPError{StatusCode: 401, Message: "Unauthorized"}
)
func fetchResource(url string) error {
return &HTTPError{
StatusCode: 404,
Message: "Resource not found",
URL: url,
Timestamp: time.Now(),
}
}
func main() {
err := fetchResource("/api/users/999")
// errors.Is works because we implemented Is()
if errors.Is(err, ErrHTTPNotFound) {
fmt.Println("Resource not found!")
}
// errors.As to access fields
var httpErr *HTTPError
if errors.As(err, &httpErr) {
fmt.Printf("Status: %d, URL: %s\n", httpErr.StatusCode, httpErr.URL)
}
}
Ошибка с методом Unwrap
type ServiceError struct {
Service string
Op string
Err error // Wrapped error
}
func (e *ServiceError) Error() string {
return fmt.Sprintf("%s.%s: %v", e.Service, e.Op, e.Err)
}
// Unwrap returns the wrapped error
func (e *ServiceError) Unwrap() error {
return e.Err
}
func getUser(id int) error {
dbErr := fmt.Errorf("connection refused")
return &ServiceError{
Service: "UserService",
Op: "GetByID",
Err: dbErr,
}
}
Sentinel Errors
Sentinel errors — предопределённые ошибки-значения для известных условий:
package mypackage
import "errors"
// Sentinel errors — package-level variables
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrTimeout = errors.New("operation timed out")
)
// Usage in the package
func Find(id string) (*Item, error) {
item, ok := store[id]
if !ok {
return nil, ErrNotFound
}
return item, nil
}
// Usage by consumers
func main() {
item, err := mypackage.Find("abc")
if errors.Is(err, mypackage.ErrNotFound) {
// Handle not found case
}
}
Стандартная библиотека содержит множество sentinel errors:
| Пакет | Ошибка | Описание |
|---|---|---|
io |
io.EOF |
Конец файла/потока |
io |
io.ErrUnexpectedEOF |
Неожиданный конец |
os |
os.ErrNotExist |
Файл не существует |
os |
os.ErrPermission |
Недостаточно прав |
os |
os.ErrExist |
Файл уже существует |
context |
context.Canceled |
Контекст отменён |
context |
context.DeadlineExceeded |
Тайм-аут |
sql |
sql.ErrNoRows |
Нет результатов |
net/http |
http.ErrServerClosed |
Сервер закрыт |
panic и recover
panic — аварийная остановка
panic прерывает нормальное выполнение программы. Используется только для невосстановимых ошибок — ошибок программиста, а не ожидаемых ситуаций.
package main
import "fmt"
func main() {
fmt.Println("Start")
// panic stops normal execution
// All deferred functions will still execute
defer fmt.Println("Deferred (will execute even after panic)")
panic("something went terribly wrong")
fmt.Println("This will never execute")
}
// Output:
// Start
// Deferred (will execute even after panic)
// panic: something went terribly wrong
// ... stack trace ...
Когда использовать panic
// OK: Initialization that cannot fail
func MustCompileRegex(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(fmt.Sprintf("invalid regex %q: %v", pattern, err))
}
return re
}
// OK: Programming error (invalid arguments)
func divide(a, b int) int {
if b == 0 {
panic("divide: division by zero") // Bug in the caller
}
return a / b
}
// OK: Unreachable code
func dayName(d int) string {
switch d {
case 0: return "Sunday"
case 1: return "Monday"
// ... all cases covered
case 6: return "Saturday"
default:
panic(fmt.Sprintf("invalid day: %d", d))
}
}
recover — перехват паники
recover перехватывает панику и позволяет программе продолжить работу. Работает только внутри deferred-функции.
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // "recovered from panic: runtime error: integer divide by zero"
} else {
fmt.Println("Result:", result)
}
}
Практический паттерн: HTTP middleware с recover
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
)
// Recovery middleware prevents server crash from panics
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// Log the stack trace
log.Printf("PANIC: %v\n%s", err, debug.Stack())
// Return 500 to the client
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
panic vs error — когда что использовать
| Ситуация | Используй | Пример |
|---|---|---|
| Файл не найден | error |
os.Open() возвращает error |
| Сеть недоступна | error |
http.Get() возвращает error |
| nil pointer dereference | panic (автоматически) |
Runtime panic |
| Index out of range | panic (автоматически) |
Runtime panic |
| Невалидные аргументы функции | panic |
Ошибка программиста |
| Инициализация с Must* | panic |
regexp.MustCompile() |
| Бизнес-логика | error |
Всегда error |
Золотое правило: Если ошибка может произойти в нормальном потоке выполнения — верните
error. Если ошибка означает баг в коде — используйтеpanic.
Лучшие практики обработки ошибок
1. Добавляйте контекст при оборачивании
// GOOD: each level adds meaningful context
func (s *UserService) GetByID(ctx context.Context, id int) (*User, error) {
user, err := s.repo.Find(ctx, id)
if err != nil {
return nil, fmt.Errorf("UserService.GetByID(%d): %w", id, err)
}
return user, nil
}
// BAD: no context added
func (s *UserService) GetByIDBad(ctx context.Context, id int) (*User, error) {
user, err := s.repo.Find(ctx, id)
if err != nil {
return nil, err // Caller won't know where the error came from
}
return user, nil
}
// BAD: redundant wrapping
func (s *UserService) GetByIDRedundant(ctx context.Context, id int) (*User, error) {
user, err := s.repo.Find(ctx, id)
if err != nil {
return nil, fmt.Errorf("error: %w", err) // "error:" adds no value
}
return user, nil
}
2. Обрабатывайте ошибку только один раз
// BAD: logging AND returning
func process() error {
err := doWork()
if err != nil {
log.Println("Error:", err) // Logged here...
return err // ...and returned for caller to log again
}
return nil
}
// GOOD: either log OR return, not both
func process() error {
err := doWork()
if err != nil {
return fmt.Errorf("process: %w", err) // Return with context
}
return nil
}
// At the top level (main, HTTP handler) — log
func main() {
if err := process(); err != nil {
log.Fatal(err) // Log at the top level
}
}
3. Используйте errors.Is/As вместо сравнения строк
// BAD: string comparison is fragile
if err.Error() == "not found" {
// ...
}
// GOOD: use sentinel errors
if errors.Is(err, ErrNotFound) {
// ...
}
// GOOD: use type assertion
var valErr *ValidationError
if errors.As(err, &valErr) {
// ...
}