· 13 мин 👁 1.4k Начинающий

Язык Go — ошибки, panic и recover

Обработка ошибок в Go — интерфейс error, кастомные типы ошибок, panic для неустранимых ситуаций и recover для перехвата паники внутри пакета.

errorspanicrecoverобработка ошибокdefer
Содержание

В Go нет исключений. Ошибки — обычные значения, которые функция возвращает явно. Это вынуждает вызывающий код обработать ошибку прямо на месте, а не надеяться что кто-то поймает исключение выше по стеку.

Интерфейс error

По соглашению ошибки имеют тип error — встроенный интерфейс:

type error interface {
    Error() string
}

Любой тип с методом Error() string удовлетворяет этому интерфейсу. Это даёт авторам библиотек свободу реализовывать ошибки с любым уровнем детализации.

Например, os.Open при ошибке возвращает не просто nil-указатель, а значение типа *os.PathError с подробным описанием что именно пошло не так:

// PathError содержит операцию, путь к файлу и исходную ошибку ОС
type PathError struct {
    Op   string // "open", "unlink" и т.д.
    Path string // путь к файлу
    Err  error  // ошибка системного вызова
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Такая ошибка выглядит так:

open /etc/passwx: no such file or directory

Это намного информативнее чем просто "no such file or directory" — даже если ошибка напечатана далеко от места где она возникла, сразу понятно что за файл, какая операция и что случилось.

Правила именования ошибок

По соглашению строка ошибки должна содержать источник — имя операции или пакета:

// хорошо: сразу понятно откуда ошибка
"image: unknown format"
"json: cannot unmarshal string into Go value of type int"
"http: request too large"

// плохо: непонятно откуда
"unknown format"
"cannot unmarshal"

Строки ошибок не начинаются с заглавной буквы и не заканчиваются точкой — они часто вставляются в более длинные сообщения через конкатенацию.

Извлечение деталей из ошибки

Если нужно получить конкретную информацию из ошибки — используем type assertion:

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return // всё хорошо
    }
    // проверяем: это PathError с ошибкой «нет места на диске»?
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles() // освобождаем место
        continue          // пробуем снова
    }
    return // другая ошибка — выходим
}

err.(*os.PathError) — type assertion: если err содержит *os.PathError, ok будет true и e даст доступ к полям структуры. Если нет — ok равно false, e равно nil, паники нет.

Panic: когда продолжение невозможно

Обычный способ сообщить об ошибке — вернуть её как значение. Но иногда программа действительно не может продолжать работу.

Для этого есть встроенная функция panic. Она принимает аргумент любого типа — обычно строку — который будет напечатан при падении программы:

// учебная реализация кубического корня методом Ньютона
func CubeRoot(x float64) float64 {
    z := x / 3 // начальное приближение
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z - x) / (3 * z * z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // миллион итераций не сошёлся — что-то принципиально не так
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

Это учебный пример. Реальные библиотечные функции должны избегать panic — если проблему можно обойти, лучше вернуть ошибку и дать вызывающему коду решить что делать.

Исключение — инициализация: если библиотека не может настроить себя при старте, panic оправдан:

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER") // без этого программа всё равно не заработает
    }
}

Recover: перехват паники

Когда вызывается panic — Go немедленно прекращает выполнение текущей функции и начинает раскручивать стек горутины, вызывая отложенные функции (defer) по пути. Если раскрутка доходит до верха стека — программа падает.

Однако встроенная функция recover позволяет перехватить панику и возобновить нормальное выполнение. recover останавливает раскрутку стека и возвращает значение переданное в panic.

recover полезен только внутри defer — это единственный код который выполняется во время раскрутки стека:

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err) // логируем и выходим чисто
        }
    }()
    do(work) // если запаникует — defer выше поймает
}

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work) // паника в одной горутине не убьёт сервер
    }
}

Если do(work) запаникует — горутина завершится чисто, ошибка будет залогирована, остальные горутины продолжат работу. Весь сервер не упадёт.

Важная деталь: recover возвращает nil если вызван не из defer или если паники не было. Это значит что отложенный код может вызывать другие функции которые внутри используют panic/recover — они не будут перехвачены внешним recover.

Паттерн: panic/recover внутри пакета

Иногда внутри сложной рекурсивной логики (например, парсера) удобно использовать panic для выхода из глубокого стека вызовов — вместо того чтобы пробрасывать ошибку через каждый уровень. Главное правило: не выпускать панику наружу из пакета.

Пример из идеализированного пакета regexp:

// Error — тип ошибки парсинга; удовлетворяет интерфейсу error
type Error string

func (e Error) Error() string {
    return string(e)
}

// error — метод *Regexp для удобного вызова panic с типом Error
// (называется error — совпадение с именем встроенного типа допустимо для методов)
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile парсит регулярное выражение и возвращает ошибку если не удалось
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // сбрасываем возвращаемое значение
            err = e.(Error) // если это не Error — будет re-panic (неожиданная ошибка)
        }
    }()
    return regexp.doParse(str), nil // doParse может вызвать panic(Error(...))
}

Как это работает:

  • doParse глубоко внутри вызывает re.error("'*' illegal at start of expression")
  • это вызывает panic(Error(...))
  • раскрутка стека поднимается до defer в Compile
  • recover() перехватывает панику и возвращает значение
  • type assertion e.(Error) проверяет: это наша ошибка парсинга?
  • если да — конвертируем в error и возвращаем вызывающему
  • если нет (например, index out of bounds) — e.(Error) паникует снова, и неожиданная ошибка продолжает раскручивать стек как ни в чём не бывало

Поэтому внутри парсера можно просто писать:

if pos == 0 {
    re.error("'*' illegal at start of expression") // без возни с возвратом ошибок через стек
}

Снаружи вызывающий код видит обычную ошибку — никакой паники не утекает из пакета.

Правило: паника не выходит за пределы пакета

Это важное соглашение в Go: если пакет использует panic внутри себя — он должен перехватить её через recover и вернуть наружу как error.

// правильно: паника перехвачена, наружу идёт error
func Parse(input string) (result Result, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("parse failed: %v", r)
        }
    }()
    return doParse(input), nil
}

// неправильно: паника утекает наружу — вызывающий код не ожидает этого
func Parse(input string) Result {
    return doParse(input) // может запаниковать — плохой API
}

Итоги

  • error — встроенный интерфейс с единственным методом Error() string. Любой тип его реализующий может быть ошибкой
  • Строки ошибок начинаются с имени пакета или операции, не начинаются с заглавной буквы, не заканчиваются точкой
  • Type assertion err.(*ConcreteType) позволяет извлечь детали из конкретного типа ошибки
  • panic — для ситуаций когда продолжение невозможно. Библиотечные функции должны избегать panic; исключение — инициализация
  • recover работает только внутри defer. Перехватывает панику и возвращает значение переданное в panic
  • Паттерн panic/recover внутри пакета: удобен для сложной рекурсивной логики, но паника не должна выходить наружу
  • e.(Error) в recover: если тип не совпадает — re-panic, неожиданные ошибки не поглощаются