Описание функций пакета Context

Описание функций пакета Context: AfterFunc, Cause, WithCancel, WithCancelCause, WithDeadline, WithDeadlineCause, WithTimeout, WithTimeoutCause

func AfterFunc

func AfterFunc(ctx Context, f func()) (stop func() bool)

AfterFunc организует вызов f в собственной горутине после отмены ctx. Если ctx уже отменен, AfterFunc вызывает f сразу в своей собственной горутине.

Несколько вызовов AfterFunc на одном контексте работают независимо; один не заменяет другой.

Вызов возвращаемой функции stop прекращает ассоциацию ctx с f. Он возвращает true, если вызов остановил запуск f. Если stop возвращает false, то либо контекст отменен и f была запущена в своей собственной goroutine, либо f уже была остановлена. Функция stop не ждет завершения f перед возвратом. Если вызывающей стороне необходимо знать, завершено ли выполнение f, она должна явно согласовать это с f.

Если у ctx есть метод “AfterFunc(func()) func() bool”, AfterFunc будет использовать его для планирования вызова.

Пример (Cond)

В этом примере AfterFunc используется для определения функции, которая ожидает синхронизацию с Cond, прекращая ожидание при отмене контекста.

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main() {
	waitOnCond := func(ctx context.Context, cond *sync.Cond, conditionMet func() bool) error {
		// Создаем канал для отмены AfterFunc
		done := make(chan struct{})
		defer close(done)

		// Настраиваем функцию отмены по таймауту контекста
		stopf := context.AfterFunc(ctx, func() {
			cond.L.Lock()
			defer cond.L.Unlock()
			select {
			case <-done:
				// Уже завершились, не нужно broadcast
				return
			default:
				// Пробуждаем все ожидающие горутины
				cond.Broadcast()
			}
		})
		defer stopf()

		// Ожидаем выполнения условия
		for !conditionMet() {
			// Проверяем контекст перед ожиданием
			if ctx.Err() != nil {
				return ctx.Err()
			}
			cond.Wait()
			// Проверяем контекст после пробуждения
			if ctx.Err() != nil {
				return ctx.Err()
			}
		}

		return nil
	}

	cond := sync.NewCond(&sync.Mutex{})

	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
			defer cancel()

			cond.L.Lock()
			defer cond.L.Unlock()

			err := waitOnCond(ctx, cond, func() bool { return false })
			fmt.Printf("Goroutine %d finished after %v with error: %v\n", 
				id, time.Since(start), err)
		}(i)
	}

	wg.Wait()
	fmt.Println("All goroutines completed")
}
Пример Connection

В этом примере используется AfterFunc для определения функции, которая считывает данные из net.Conn, останавливая считывание при отмене контекста.

package main

import (
	"context"
	"fmt"
	"net"
	"time"
)

func readFromConn(ctx context.Context, conn net.Conn, b []byte) (n int, err error) {
	stopc := make(chan struct{})
	defer close(stopc) // Гарантируем закрытие канала

	// Устанавливаем функцию отмены чтения при отмене контекста
	stop := context.AfterFunc(ctx, func() {
		conn.SetReadDeadline(time.Now()) // Прерываем текущее чтение
		close(stopc)
	})
	defer func() {
		if !stop() {
			// Если AfterFunc был запущен, сбрасываем дедлайн
			<-stopc
			conn.SetReadDeadline(time.Time{})
		}
	}()

	n, err = conn.Read(b)
	if ctx.Err() != nil {
		return 0, ctx.Err() // Возвращаем ошибку контекста если он отменен
	}
	return n, err
}

func main() {
	// Создаем тестовый сервер
	listener, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		fmt.Println("Failed to create listener:", err)
		return
	}
	defer listener.Close()

	// Устанавливаем соединение
	conn, err := net.Dial(listener.Addr().Network(), listener.Addr().String())
	if err != nil {
		fmt.Println("Failed to dial:", err)
		return
	}
	defer conn.Close()

	// Создаем контекст с таймаутом
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()

	// Читаем данные
	b := make([]byte, 1024)
	_, err = readFromConn(ctx, conn, b)
	
	// Проверяем тип ошибки
	switch {
	case err == nil:
		fmt.Println("Read completed successfully")
	case ctx.Err() != nil:
		fmt.Println("Operation canceled:", ctx.Err())
	default:
		fmt.Println("Read error:", err)
	}
}

Дополнительные рекомендации:

  1. Таймауты соединения:

    conn.SetDeadline(time.Now().Add(30 * time.Second))
    
  2. Буферизация:

    bufReader := bufio.NewReader(conn)
    n, err = bufReader.Read(b)
    
  3. Повторные попытки:

    for retries := 0; retries < 3; retries++ {
        n, err = readFromConn(ctx, conn, b)
        if err == nil || !isTemporary(err) {
            break
        }
    }
    
  4. Логирование:

    log.Printf("Read %d bytes, error: %v", n, err)
    
Пример Merge

В этом примере AfterFunc используется для определения функции, которая объединяет сигналы отмены двух Контекстов.

package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
)

func main() {
	// mergeCancel возвращает контекст, который отменяется при отмене любого из исходных контекстов
	mergeCancel := func(ctx1, ctx2 context.Context) (context.Context, context.CancelFunc) {
		mergedCtx, cancel := context.WithCancelCause(ctx1)
		var once sync.Once

		// Отслеживаем отмену первого контекста
		go func() {
			select {
			case <-ctx1.Done():
				once.Do(func() {
					cancel(context.Cause(ctx1))
				})
			case <-ctx2.Done():
				once.Do(func() {
					cancel(context.Cause(ctx2))
				})
			case <-mergedCtx.Done():
				// Уже отменен другим путем
			}
		}()

		return mergedCtx, func() {
			once.Do(func() {
				cancel(context.Canceled)
			})
		}
	}

	// Создаем два контекста с возможностью отмены
	ctx1, cancel1 := context.WithCancelCause(context.Background())
	defer cancel1(errors.New("ctx1 canceled"))

	ctx2, cancel2 := context.WithCancelCause(context.Background())

	// Объединяем контексты
	mergedCtx, mergedCancel := mergeCancel(ctx1, ctx2)
	defer mergedCancel()

	// Отменяем второй контекст
	cancel2(errors.New("ctx2 canceled"))

	// Ждем отмены объединенного контекста
	<-mergedCtx.Done()
	
	// Выводим причину отмены
	fmt.Println("Merged context canceled because:", context.Cause(mergedCtx))
}

Ключевые моменты:

  1. Потокобезопасность:

    var once sync.Once
    once.Do(func() {
        cancel(context.Cause(ctx))
    })
    
  2. Полное отслеживание контекстов:

    select {
    case <-ctx1.Done():
    case <-ctx2.Done():
    case <-mergedCtx.Done():
    }
    
  3. Четкая причина отмены:

    fmt.Println("Merged context canceled because:", context.Cause(mergedCtx))
    
  4. Гарантированная очистка:

    defer mergedCancel()
    

func Cause

func Cause(c Context) error

Cause возвращает не нулевую ошибку, объясняющую, почему c был отменен. Первая отмена c или одного из его родителей устанавливает причину. Если эта отмена произошла через вызов CancelCauseFunc(err), то Cause возвращает err. В противном случае Cause(c) возвращает то же значение, что и c.Err(). Cause возвращает nil, если c еще не был отменен.

func WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel возвращает производный контекст, который указывает на родительский контекст, но имеет новый канал Done. Канал Done возвращенного контекста закрывается при вызове возвращенной функции отмены или при закрытии канала Done родительского контекста, в зависимости от того, что произойдет раньше.

Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся.

Пример

Этот пример демонстрирует использование отменяемого контекста для предотвращения утечки данных из горутины. К концу выполнения функции примера горутина, запущенная gen, вернется без утечки.

package main

import (
	"context"
	"fmt"
	"sync"
)

func main() {
	// gen генерирует целые числа в отдельной горутине и
	// отправляет их в возвращаемый канал.
	// Вызывающая сторона должна отменить контекст после
	// завершения потребления чисел, чтобы избежать утечки горутины.
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		var wg sync.WaitGroup
		wg.Add(1)

		go func() {
			defer wg.Done()
			defer close(dst) // Закрываем канал при завершении
			n := 1
			for {
				select {
				case <-ctx.Done():
					return
				case dst <- n:
					n++
				}
			}
		}()

		// Горутина для безопасного завершения
		go func() {
			wg.Wait()
		}()

		return dst
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // Отменяем контекст при завершении

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}

	// Дополнительная отмена контекста (хотя defer уже сделает это)
	cancel()
}

func WithCancelCause

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)

WithCancelCause ведет себя как WithCancel, но возвращает CancelCauseFunc вместо CancelFunc. Вызов cancel с no-nil ошибкой (причина) записывает эту ошибку в ctx; затем ее можно получить с помощью Cause(ctx). Вызов cancel с nil устанавливает причину в Canceled.

Пример использования:

ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // возвращает context.Canceled
context.Cause(ctx) // возвращает myError

func WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline возвращает производный контекст, который указывает на родительский контекст, но имеет срок, скорректированный так, чтобы он не был позднее d. Если срок родительского контекста уже раньше d, WithDeadline(parent, d) семантически эквивалентен parent. Возвращаемый канал [Context.Done] закрывается по истечении срока, при вызове возвращаемой функции cancel или при закрытии канала Done родительского контекста, в зависимости от того, что произойдет раньше.

Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся.

Пример

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

package main

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

func main() {
	shortDuration := 50 * time.Millisecond // Явное указание времени таймаута
	d := time.Now().Add(shortDuration)
	
	// Создаем контекст с дедлайном
	ctx, cancel := context.WithDeadline(context.Background(), d)
	defer cancel() // Важно вызывать cancel для освобождения ресурсов

	neverReady := make(chan struct{}) // Канал, который никогда не будет готов

	select {
	case <-neverReady:
		fmt.Println("ready") // Эта ветка никогда не выполнится
	case <-ctx.Done():
		// Проверяем причину завершения контекста
		switch ctx.Err() {
		case context.DeadlineExceeded:
			fmt.Println("context deadline exceeded")
		case context.Canceled:
			fmt.Println("context canceled")
		default:
			fmt.Println("context done for unknown reason")
		}
	}
}

func WithDeadlineCause

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)

WithDeadlineCause ведет себя как WithDeadline, но также устанавливает причину возвращаемого контекста при превышении срока. Возвращаемая CancelFunc не устанавливает причину.

func WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout возвращает WithDeadline(parent, time.Now().Add(timeout)).

Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся:

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // освобождает ресурсы, если slowOperation завершается до истечения времени ожидания
	return slowOperation(ctx)
}
Пример

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

package main

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

func main() {
	shortDuration := 100 * time.Millisecond // Явное указание времени таймаута

	// Создаем контекст с таймаутом
	ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
	defer cancel() // Всегда вызываем cancel для освобождения ресурсов

	neverReady := make(chan struct{}) // Создаем канал, который никогда не закроется

	select {
	case <-neverReady:
		fmt.Println("ready") // Эта ветка никогда не выполнится
	case <-ctx.Done():
		// Проверяем причину завершения контекста
		switch err := ctx.Err(); err {
		case context.DeadlineExceeded:
			fmt.Println("context deadline exceeded")
		case context.Canceled:
			fmt.Println("context canceled")
		default:
			fmt.Printf("context done: %v\n", err)
		}
	}
}

func WithTimeoutCause

func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)

WithTimeoutCause ведет себя как WithTimeout, но также устанавливает причину возвращаемого Context при истечении таймаута. Возвращаемый CancelFunc не устанавливает причину.