Irbisa · Шпаргалка

Ошибки и паники

error как значение, обёртка через %w, errors.Is/As, panic и recover

Junior-разработчик · 21 вопрос

  1. 01

    Как устроена обработка ошибок в Go: что такое тип error и почему Go возвращает ошибки, а не бросает исключения?

    Функция, которая может завершиться неудачей, возвращает error последним результатом, а вызывающий сразу проверяет if err != nil; error представляет собой встроенный интерфейс с единственным методом Error() string.

    func parsePort(s string) (int, error) {	n, err := strconv.Atoi(s)	if err != nil {		return 0, fmt.Errorf("parse port %q: %w", s, err)	}	if n < 1 || n > 65535 {…
  2. 02

    Что такое sentinel-ошибка, как её объявить и сравнить и почему errors.New("x") == errors.New("x") даёт false?

    Sentinel представляет собой ошибку-переменную уровня пакета вроде io.EOF или sql.ErrNoRows, с которой сравнивают вызывающие.

    var ErrUserNotFound = errors.New("user not found") func main() {	a, b := errors.New("user not found"), errors.New("user not found")	fmt.Println(a == b)                 // false: two distinct pointers	fmt.Println(a.Error() == b.Error()) // true: same text, and irrelevant…
  3. 03

    Какие соглашения есть для текста ошибки в Go и как добавить к ошибке контекст, прежде чем вернуть её выше?

    Текст ошибки пишут как фрагмент, а не предложение: с маленькой буквы, без точки в конце, без failed to.

  4. 04

    Как создать свой тип ошибки и когда тип лучше, чем errors.New или fmt.Errorf?

    Любой тип с методом Error() string является ошибкой.

    type ValidationError struct {	Field  string	Reason string} func (e *ValidationError) Error() string {…
  5. 05

    Как работают panic и recover и где нужно вызвать recover, чтобы он перехватил панику?

    panic прерывает обычное выполнение и раскручивает текущую горутину, по пути выполняя её отложенные вызовы.

    func SafeDo(job Job) (err error) {	defer func() {		if r := recover(); r != nil { // called directly by the deferred func: works			err = fmt.Errorf("job %s panicked: %v\n%s", job.ID, r, debug.Stack())		}	}()…
  6. 06

    Чем отличаются panic, os.Exit и log.Fatal и что в каждом случае происходит с отложенными вызовами?

    panic раскручивает стек горутины и выполняет её отложенные вызовы, поэтому recover может её остановить; os.Exit завершает процесс на месте и не выполняет ни одного defer; log.Fatal сводится к строке лога плюс os.Exit(1).

    func main() {	if err := run(); err != nil {		fmt.Fprintln(os.Stderr, "report:", err)		os.Exit(1) // every defer inside run has already finished	}}…
  7. 07

    Когда допустимо вызвать panic вместо возврата ошибки и что такое соглашение Must?

    Панику оставляют для ошибок программиста, то есть состояний, означающих, что неверна сама программа; для всего, что может сломаться во время работы, возвращают error.

  8. 08

    Чем отличается fmt.Errorf с %w от fmt.Errorf с %v и когда стоит сознательно взять %v?

    %w оборачивает: новая ошибка хранит исходное значение и получает метод Unwrap, поэтому errors.Is и errors.As по-прежнему находят причину.

    func main() {	lost := fmt.Errorf("query user: %v", sql.ErrNoRows)	kept := fmt.Errorf("query user: %w", sql.ErrNoRows) 	fmt.Println(lost.Error() == kept.Error())   // true: same text	fmt.Println(errors.Is(lost, sql.ErrNoRows)) // false: chain cut…
  9. 09

    Что напечатает эта программа и где errors.Is прекращает обход цепочки?

    var ErrNotFound = errors.New("not found") func main() {	err := fmt.Errorf("get order 7: %w", ErrNotFound)	fmt.Println(err == ErrNotFound)	fmt.Println(errors.Is(err, ErrNotFound)) 	cut := fmt.Errorf("handler: %v", err)	fmt.Println(errors.Is(cut, ErrNotFound)) 	_, openErr := os.Open("/no/such/file")	wrapped := fmt.Errorf("load config: %w", openErr)	fmt.Println(os.IsNotExist(wrapped), errors.Is(wrapped, fs.ErrNotExist))}

    Она напечатает false, true, false, затем false true.

  10. 10

    Как достать типизированную ошибку вроде *ValidationError из обёрнутой цепочки и почему утверждение типа перестаёт работать, как только добавили контекст?

    Используйте errors.As(err, &target): он обходит ту же цепочку, что и errors.Is, и присваивает первую ошибку нужного типа.

    type ValidationError struct{ Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func main() {	var err error = &ValidationError{Field: "email"}…
  11. 11

    Как сделать, чтобы ваш тип ошибки работал с errors.Is и errors.As: что делают методы Unwrap и Is?

    Дайте типу метод Unwrap() error, который возвращает причину, и тогда errors.Is и errors.As пройдут сквозь него.

    type NotFound struct {	Key   string	cause error} func (e *NotFound) Error() string { return "not found: " + e.Key }…
  12. 12

    Как объединить несколько ошибок в одну и найдёт ли errors.Is каждую из них?

    Используйте errors.Join (Go 1.20+) или несколько глаголов %w в одном fmt.Errorf.

    var (	ErrMissingField = errors.New("missing field")	ErrBadFormat    = errors.New("bad format")) func Validate(c Config) error {…
  13. 13

    Эта функция компилируется и проходит тесты. Что не так с обработкой ошибок в ней?

    func SaveReport(path string, r Report) error {	f, err := os.Create(path)	if err != nil {		log.Printf("create report: %v", err)		return err	}	defer f.Close() 	data, _ := json.Marshal(r)	if _, err := f.Write(data); err != nil {		return errors.New("Failed to write report: " + err.Error())	}	return nil}

    Четыре вещи: она и логирует ошибку, и возвращает её, отбрасывает ошибку json.Marshal, теряет причину ошибки записи, склеивая текст, и игнорирует ошибку Close у файла, в который писала.

  14. 14

    Найдите баг: Transfer переводит деньги, но каждый вызов возвращает ошибку, и клиенты повторяют запрос.

    func (s *Store) Transfer(ctx context.Context, from, to int64, amount int) error {	tx, err := s.db.BeginTx(ctx, nil)	if err != nil {		return fmt.Errorf("transfer: begin: %w", err)	}	defer tx.Rollback() 	if err := debit(ctx, tx, from, amount); err != nil {		return fmt.Errorf("transfer: debit %d: %w", from, err)	}	if err := credit(ctx, tx, to, amount); err != nil {		return fmt.Errorf("transfer: credit %d: %w", to, err)	}	return fmt.Errorf("transfer: commit: %w", tx.Commit())}

    fmt.Errorf возвращает не-nil ошибку, даже если в %w передан nil, поэтому успешный коммит возвращается как transfer: commit: %!w(<nil>).

  15. 15

    Что происходит, когда горутина паникует, может ли другая горутина её восстановить и как защитить горутины, которые вы запускаете?

    Невосстановленная паника в любой горутине роняет весь процесс, и остановить её может только recover в отложенных вызовах той же горутины; middleware восстановления или recover в родителе её не увидят.

    // Go runs fn in a new goroutine and turns a panic into a logged error.func Go(ctx context.Context, name string, fn func(context.Context)) {	go func() {		defer func() {			if r := recover(); r != nil {				slog.ErrorContext(ctx, "goroutine panicked",…
  16. 16

    Найдите баг: когда задача паникует, воркер продолжает работать, но задача засчитывается как успешная.

    // runJob runs one job; a panic inside it must come back as an error.func runJob(job Job) error {	var err error	defer func() {		if r := recover(); r != nil {			err = fmt.Errorf("job %s panicked: %v", job.ID, r)		}	}()	err = job.Run()	return err}

    Результат runJob безымянный, поэтому отложенное замыкание пишет в локальную err, которую никто не возвращает.

  17. 17

    Есть ли у ошибок в Go стек-трейс? Как его получить, когда он нужен, и почему стандартная библиотека его не добавляет?

    Нет: ошибка из errors.New или fmt.Errorf хранит только сообщение и обёрнутую причину.

    type stackError struct {	err error	pcs []uintptr} func (e *stackError) Error() string { return e.err.Error() }…
  18. 18

    Как сопоставлять доменные ошибки с кодами HTTP или gRPC, не раскрывая внутренности, и где должно жить это сопоставление?

    В одной функции на транспортном слое: домен возвращает свои sentinel'ы и типизированные ошибки, а хендлер или интерсептор переводит их через errors.Is и errors.As в статус и безопасное сообщение, отдавая 500 или codes.Internal для всего неизвестного.

    // httpError translates a domain error into a status and a client-safe message.func httpError(err error) (int, string) {	var ve *ValidationError	switch {	case errors.As(err, &ve):		return http.StatusBadRequest, "invalid " + ve.Field…
  19. 19

    Как реализовать повтор с экспоненциальным backoff и ограничением числа попыток, который останавливается сразу после отмены контекста?

    Цикл максимум на N попыток: возврат при успехе или при ошибке, которая не исправится со временем, а между попытками идёт ожидание в select на ctx.Done() и таймере, чья задержка каждый раз удваивается, с потолком и случайным джиттером.

    func Retry(ctx context.Context, attempts int, base time.Duration, op func(context.Context) error) error {	var err error	delay := base	for i := range attempts {		if err = op(ctx); err == nil || !retryable(err) {			return err…
  20. 20

    Как спроектировать API ошибок публичного пакета (sentinel-значения, экспортируемые типы или интерфейсы поведения) и к чему обязывает каждый вариант?

    Любая ошибка, на которую вызывающий может матчиться через errors.Is или errors.As, становится частью вашего API, такой же обязывающей, как сигнатура функции.

    // Only the behaviour is promised; notFoundError stays private and free to change.type notFoundError struct {	Resource, Key string} func (e *notFoundError) Error() string  { return e.Resource + " " + e.Key + " not found" }…
  21. 21

    Что, кроме вызова recover, должен делать правильно middleware восстановления в HTTP (вспомните http.ErrAbortHandler и ответ, который уже наполовину записан)?

    Он должен перебрасывать http.ErrAbortHandler, которым хендлеры намеренно прерывают ответ, и писать 500 только если ещё ничего не отправлено.

    type recorder struct {	http.ResponseWriter	wrote bool} func (r *recorder) WriteHeader(code int) { r.wrote = true; r.ResponseWriter.WriteHeader(code) }…