Какие нововведения, связанные с отменой контекста (cancellation), появились в пакете `context` в Go 1.21? Расскажите о новых функциях и их практической пользе.

Ответ

В Go 1.21 был добавлен механизм для указания и распространения причины (cause) отмены контекста. Ранее ctx.Err() мог вернуть только context.Canceled или context.DeadlineExceeded, не давая информации о том, почему произошла отмена.

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

  1. WithCancelCause(parent Context) (ctx Context, cancel func(cause error)) Создает контекст, который можно отменить, передав причину в функцию cancel.

  2. WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) Аналог WithDeadline, но позволяет сразу указать причину, которая будет использована при истечении дедлайна.

  3. WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) Аналог WithTimeout, но с возможностью указать причину для таймаута.

Для получения этой причины используется новая функция context.Cause(ctx) error.

Пример:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func main() {
    // Задаем кастомную ошибку как причину
    processingErr := errors.New("processing took too long")

    // Создаем контекст с таймаутом и причиной
    ctx, cancel := context.WithTimeoutCause(
        context.Background(),
        time.Second,
        processingErr,
    )
    defer cancel()

    // Имитируем операцию, которая длится дольше таймаута
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("operation completed successfully")
    case <-ctx.Done():
        // ctx.Err() вернет стандартную ошибку
        fmt.Printf("Context done. Error: %vn", ctx.Err()) // -> context deadline exceeded

        // context.Cause() вернет нашу кастомную ошибку
        cause := context.Cause(ctx)
        fmt.Printf("Cause of cancellation: %vn", cause) // -> processing took too long

        // Мы можем проверить, что причина - это именно наша ошибка
        if errors.Is(cause, processingErr) {
            fmt.Println("The cause is the expected processing error.")
        }
    }
}

Практическая польза:

  • Улучшенная отладка и логирование: Вместо общего сообщения об отмене можно логировать конкретную причину, что упрощает анализ проблем.
  • Гранулярная обработка ошибок: Появляется возможность по-разному реагировать на разные причины отмены. Например, отмена по таймауту может требовать повторной попытки, а отмена из-за ошибки валидации — немедленного прекращения.
  • Взаимодействие в распределенных системах: При отмене запроса в микросервисной архитектуре можно передать осмысленную ошибку через цепочку вызовов, а не просто сигнал отмены.

Ответ 18+ 🔞

Да ты посмотри, что эти ребята из Go придумали! В версии 1.21, блядь, подвезли фичу, про которую все давно орали. Раньше контекст, сука, отменялся, и всё, что ты мог выцепить — это context.Canceled или context.DeadlineExceeded. А хули случилось-то? Почему отмена? Кто виноват? Пиздец, темнота, как в жопе у крота!

А теперь, ёпта, можно прицепить к отмене причину, конкретную ошибку! Наконец-то, блядь, свет в конце тоннеля! Добавили три новые функции, смотри:

  1. WithCancelCause(parent Context) (ctx Context, cancel func(cause error)) Создаёт контекст, который можно отменить, и в функцию cancel теперь можно впихнуть причину, типа cancel(errors.New("пошёл нахуй, сервер сгорел")).

  2. WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) То же самое, что WithDeadline, но сразу говоришь, какую ошибку вернёт, когда время выйдет. Не "дедлайн превышен", а "дедлайн превышен, потому что ты, мудила, в БД 10 секунд запрос гонял".

  3. WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) Ну это, ясень пень, аналог WithTimeout, но тоже с причиной.

А чтобы эту причину потом выковырять, есть новая функция context.Cause(ctx) error. Вот так вот, блядь, элегантно!

Смотри, как это в деле, на живом примере:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func main() {
    // Придумываем свою, душевную ошибку-причину
    processingErr := errors.New("processing took too long")

    // Создаём контекст с таймаутом и сразу говорим, по какой причине он сработает
    ctx, cancel := context.WithTimeoutCause(
        context.Background(),
        time.Second,
        processingErr, // Вот она, родная!
    )
    defer cancel()

    // Допустим, у нас тут операция, которая зависла на два часа
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("operation completed successfully")
    case <-ctx.Done():
        // Старый добрый ctx.Err() вернёт стандартное говно
        fmt.Printf("Context done. Error: %vn", ctx.Err()) // -> context deadline exceeded

        // А вот новый красавчик context.Cause() докопается до сути!
        cause := context.Cause(ctx)
        fmt.Printf("Cause of cancellation: %vn", cause) // -> processing took too long

        // И мы можем проверить, что это именно наша ошибка, а не какая-то левая!
        if errors.Is(cause, processingErr) {
            fmt.Println("The cause is the expected processing error.")
        }
    }
}

И зачем это всё, спросишь?

  • Логирование и отладка: Теперь в логах будет не просто "ой, всё", а конкретно "ой, всё, потому что сеть легла". Красота, блядь!
  • Умная обработка: Можно по-разному реагировать на разные причины. Отмена из-за таймаута — перезапросик сделать. Отмена потому что пользователь нахуй послал — всё, закругляемся.
  • Микросервисы: В цепочке вызовов можно протащить внятную ошибку через все контексты, а не просто сухой сигнал отмены. Все будут в курсе, кто тут, блядь, виноват.

В общем, фича — огонь. Наконец-то контексты заговорили человеческим языком, а не мычанием!