Обработка ошибок как философия
Обработка ошибок как философия
Go отказался от исключений в пользу явной обработки ошибок. Поначалу кажется многословным, но это делает код предсказуемым и надежным.
Основы error
Интерфейс error
error - это обычный интерфейс, а не магия:
type error interface {
Error() string
}
// Использование
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль")
}
return a / b, nil
}
// Проверка
result, err := divide(10, 0)
if err != nil {
log.Fatal(err) // обработка ошибки
}
Создание ошибок
errors.New
import "errors"
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
// поиск пользователя...
return nil, ErrNotFound
}
fmt.Errorf
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("возраст не может быть отрицательным: %d", age)
}
if age > 150 {
return fmt.Errorf("невероятный возраст: %d", age)
}
return nil
}
Обертывание ошибок (Go 1.13+)
func readConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать конфиг %s: %w", filename, err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("не удалось распарсить конфиг: %w", err)
}
return &config, nil
}
// Проверка обернутых ошибок
if err := readConfig("app.json"); err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Println("Конфиг не найден, используем дефолтный")
} else {
log.Fatal(err)
}
}
errors.Is проходит цепочку слева направо через Unwrap() - от верхнего слоя до причины. errors.As находит в цепи первое значение нужного типа и присваивает его.
Проверка ошибок
errors.Is
// Проверка на конкретную ошибку
if errors.Is(err, os.ErrNotExist) {
// файл не существует
}
// Работает через всю цепочку обернутых ошибок
err1 := errors.New("original")
err2 := fmt.Errorf("wrapped: %w", err1)
err3 := fmt.Errorf("double wrapped: %w", err2)
fmt.Println(errors.Is(err3, err1)) // true
errors.As
// Своя ошибка с дополнительными данными
type ValidationError struct {
Field string
Value interface{}
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %v", e.Field, e.Value)
}
// Проверка типа ошибки
var validErr *ValidationError
if errors.As(err, &validErr) {
fmt.Printf("Поле %s невалидно\n", validErr.Field)
}
Паттерны обработки
Early return
В примере ниже - связка early-return и defer (отложенное закрытие ресурса):
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("открытие файла: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("чтение файла: %w", err)
}
if err := processData(data); err != nil {
return fmt.Errorf("обработка данных: %w", err)
}
return nil
}
Группировка проверок
// Когда много похожих проверок
func createUser(name, email string, age int) (*User, error) {
if err := validateName(name); err != nil {
return nil, err
}
if err := validateEmail(email); err != nil {
return nil, err
}
if err := validateAge(age); err != nil {
return nil, err
}
return &User{name, email, age}, nil
}
Helper для fatal ошибок
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
// Использование в main или init
func main() {
config, err := loadConfig()
must(err)
db, err := connectDB(config.DatabaseURL)
must(err)
defer db.Close()
}
Создание своих типов ошибок
С дополнительным контекстом
type HTTPError struct {
Code int
Message string
Details map[string]interface{}
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
func (e *HTTPError) StatusCode() int {
return e.Code
}
// Использование
func handleRequest(w http.ResponseWriter, r *http.Request) {
user, err := getUser(r.Context(), userID)
if err != nil {
var httpErr *HTTPError
if errors.As(err, &httpErr) {
http.Error(w, httpErr.Error(), httpErr.StatusCode())
} else {
http.Error(w, "Internal Server Error", 500)
}
return
}
}
Множественные ошибки
type MultiError struct {
errors []error
}
func (m *MultiError) Error() string {
var msgs []string
for _, err := range m.errors {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
func (m *MultiError) Add(err error) {
if err != nil {
m.errors = append(m.errors, err)
}
}
func (m *MultiError) HasErrors() bool {
return len(m.errors) > 0
}
Sentinel errors
// Определяем глобальные ошибки для сравнения
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrTokenExpired = errors.New("token expired")
)
func authenticate(username, password string) error {
user, err := findUser(username)
if err != nil {
return err
}
if !checkPassword(user, password) {
return ErrInvalidPassword
}
return nil
}
// Использование
if err := authenticate("alice", "wrongpass"); err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
// обработка
case errors.Is(err, ErrInvalidPassword):
// обработка
default:
// неизвестная ошибка
}
}
Best practices
<ComparisonTable data={{ headers: ["Практика", "Плохо", "Хорошо"], rows: [ ["Игнорирование", "result, _ := doSomething()", "result, err := doSomething()\nif err != nil { ... }"], ["Контекст", "return err", "return fmt.Errorf("processing user %d: %w", userID, err)"], ["Sentinel errors", "errors.New("not found") каждый раз", "var ErrNotFound = errors.New("not found")"], ["Паника", "panic(err) // в библиотеке", "return err // паника только для невосстановимых"], ["Логирование", "log.Println(err); return err", "return err // логируем на верхнем уровне"] ] }} />
Когда использовать panic
// Panic уместен для невосстановимых ошибок программирования
func MustCompile(pattern string) *Regexp {
re, err := Compile(pattern)
if err != nil {
panic(err) // программист передал невалидный паттерн
}
return re
}
// В init функциях
func init() {
template = template.Must(template.ParseFiles("template.html"))
}
// Но НЕ для обычных runtime ошибок!
Обработка panic
func safeFunction() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
// код, который может паниковать
riskyOperation()
return nil
}
Практический пример: API клиент
type APIError struct {
StatusCode int
Message string
RequestID string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s (request: %s)",
e.StatusCode, e.Message, e.RequestID)
}
func (e *APIError) Temporary() bool {
return e.StatusCode == 429 || e.StatusCode >= 500
}
type Client struct {
baseURL string
httpClient *http.Client
}
func (c *Client) GetUser(ctx context.Context, id string) (*User, error) {
url := fmt.Sprintf("%s/users/%s", c.baseURL, id)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("создание запроса: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("выполнение запроса: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: resp.Status,
RequestID: resp.Header.Get("X-Request-ID"),
}
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("декодирование ответа: %w", err)
}
return &user, nil
}
Итоги
- Ошибки в Go явные, не исключения
- Всегда обрабатывайте ошибки
- Добавляйте контекст при обертывании
- Используйте errors.Is и errors.As
- panic только для невосстановимых ситуаций
В следующем уроке погрузимся в конкурентность - горутины!
Типичная ошибка
Игнорировать ошибку через _ - «и так работает». Работает... пока не прод. Всегда проверяй err != nil.