Описание пакета context языка программирования Go
Пакет Context определяет тип Context, который передает сроки, сигналы отмены и другие значения в рамках запроса через границы API и между процессами.
Входящие запросы к серверу должны создавать Context, а исходящие вызовы к серверам должны принимать Context. Цепочка вызовов функций между ними должна передавать Context, при необходимости заменяя его производным Context, созданным с помощью WithCancel, WithDeadline, WithTimeout или WithValue.
Контекст может быть отменен, чтобы указать, что работа, выполняемая от его имени, должна быть остановлена. Контекст с крайним сроком отменяется после истечения крайнего срока. Когда контекст отменяется, все производные от него контексты также отменяются.
Функции WithCancel, WithDeadline и WithTimeout принимают контекст (родительский) и возвращают производный контекст (дочерний) и CancelFunc. Прямой вызов CancelFunc отменяет дочерний контекст и его дочерние контексты, удаляет ссылку родительского контекста на дочерний и останавливает все связанные таймеры. Невыполнение вызова CancelFunc приводит к утечке дочернего контекста и его дочерних контекстов до тех пор, пока не будет отменен родительский контекст. Инструмент go vet
проверяет, что CancelFuncs используются на всех путях управления потоком.
Функции WithCancelCause, WithDeadlineCause и WithTimeoutCause возвращают CancelCauseFunc, который принимает ошибку и записывает ее как причину отмены. Вызов Cause на отмененном контексте или любом из его дочерних элементов извлекает причину. Если причина не указана, Cause(ctx) возвращает то же значение, что и ctx.Err().
Программы, использующие контексты, должны следовать этим правилам, чтобы обеспечить согласованность интерфейсов между пакетами и позволить инструментам статического анализа проверять распространение контекста:
Не храните контексты внутри типа struct; вместо этого явно передавайте контекст каждой функции, которой он нужен. Это подробнее обсуждается в разделе «https://go.dev/blog/context-and-structs». Контекст должен быть первым параметром, обычно называемым ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... использовать ctx ...
}
Не передавайте nil Context, даже если функция это допускает. Передайте context.TODO, если не уверены, какой Context использовать.
Используйте значения контекста только для данных в рамках запроса, которые проходят через процессы и API, а не для передачи опциональных параметров функциям.
Один и тот же Context может передаваться функциям, выполняющимся в разных goroutines; Context безопасен для одновременного использования несколькими goroutines.
См. https://go.dev/blog/context для примера кода сервера, использующего Context.
Переменные пакета
var Canceled = errors.New(«context canceled»)
Canceled — это ошибка, возвращаемая [Context.Err], когда контекст отменяется по какой-либо причине, кроме истечения срока.
var DeadlineExceeded error = deadlineExceededError{}
DeadlineExceeded — это ошибка, возвращаемая [Context.Err], когда контекст отменяется из-за истечения срока.
1 - Описание функций пакета 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)
}
}
Дополнительные рекомендации:
-
Таймауты соединения:
conn.SetDeadline(time.Now().Add(30 * time.Second))
-
Буферизация:
bufReader := bufio.NewReader(conn)
n, err = bufReader.Read(b)
-
Повторные попытки:
for retries := 0; retries < 3; retries++ {
n, err = readFromConn(ctx, conn, b)
if err == nil || !isTemporary(err) {
break
}
}
-
Логирование:
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))
}
Ключевые моменты:
-
Потокобезопасность:
var once sync.Once
once.Do(func() {
cancel(context.Cause(ctx))
})
-
Полное отслеживание контекстов:
select {
case <-ctx1.Done():
case <-ctx2.Done():
case <-mergedCtx.Done():
}
-
Четкая причина отмены:
fmt.Println("Merged context canceled because:", context.Cause(mergedCtx))
-
Гарантированная очистка:
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
не устанавливает причину.
2 - Описание типов пакета Context
Описание типов пакета Context
type CancelCauseFunc
type CancelCauseFunc func(cause error)
CancelCauseFunc
ведет себя как CancelFunc
, но дополнительно устанавливает причину отмены. Эту причину можно получить, вызвав Cause
на отмененном Context
или на любом из его производных Context
.
Если контекст уже был отменен, CancelCauseFunc
не устанавливает причину. Например, если childContext
является производным от parentContext
:
- если parentContext отменен с причиной cause1 до того, как childContext отменен с причиной cause2, то Cause(parentContext) == Cause(childContext) == cause1
- если childContext отменен с причиной cause2 до того, как parentContext отменен с причиной cause1, то Cause(parentContext) == cause1 и Cause(childContext) == cause2
type CancelFunc
CancelFunc
сообщает операции о необходимости прекратить работу. CancelFunc
не ждет, пока работа будет остановлена. CancelFunc
может вызываться несколькими goroutines
одновременно. После первого вызова последующие вызовы CancelFunc
ничего не делают.
type Context
type Context interface {
// Deadline возвращает время, когда работа, выполняемая от имени этого
// контекста должна быть отменена. Deadline возвращает ok==false, если
// срок не установлен. Последовательные вызовы Deadline возвращают
// одинаковые результаты.
Deadline() (deadline time.Time, ok bool)
// Done возвращает канал, который закрывается, когда работа, выполняемая
// от имени этого контекста, должна быть отменена. Done может возвращать
// nil, если этот контекст никогда не может быть отменен. Последовательные
// вызовы Done возвращают одинаковое значение.
// Закрытие канала Done может происходить асинхронно,
// после возврата функции cancel.
//
// WithCancel организует закрытие Done при вызове cancel;
// WithDeadline организует закрытие Done по истечении срока;
// WithTimeout организует закрытие Done по истечении таймаута
//.
//
// Done предоставляется для использования в операторах select:
//
// // Stream генерирует значения с помощью DoSomething и отправляет их в out,
// // пока DoSomething не вернет ошибку или ctx.Done не будет закрыт.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// См. https://blog.golang.org/pipelines для получения дополнительных примеров
// использования канала Done для отмены.
Done() <-chan struct{}
// Если Done еще не закрыт, Err возвращает nil.
// Если Done закрыт, Err возвращает no-nil ошибку, объясняющую причину:
// DeadlineExceeded, если срок контекста истек, или Canceled, если контекст
// был отменен по какой-либо другой причине.
// После того, как Err возвращает ошибку, отличную от nil, последующие вызовы
// Err возвращают ту же ошибку.
Err() error
// Value возвращает значение, связанное с этим контекстом для ключа, или nil,
// если с ключом не связано никакое значение. Последовательные вызовы Value с
// одним и тем же ключом возвращают один и тот же результат.
//
// Используйте значения контекста только для данных в рамках запроса, которые
// проходят через процессы и границы API, а не для передачи опциональных
// параметров в функции.
//
// Ключ идентифицирует конкретное значение в контексте. Функции, которые хотят
// сохранить значения в контексте, обычно выделяют ключ в глобальной
// переменной, а затем используют этот ключ в качестве аргумента для
// context.WithValue и Context.Value. Ключ может быть любого типа, который
// поддерживает равенство;
// пакеты должны определять ключи как неэкспортируемый тип, чтобы избежать коллизий.
//
// Пакеты, которые определяют ключ контекста, должны предоставлять типобезопасные
// аксессоры для значений, хранящихся с использованием этого ключа:
//
// // Пользователь пакета определяет тип User, который хранится в контекстах.
// пакет user
//
// import «context»
//
// // User — это тип значения, хранящегося в Contexts.
// type User struct {...}
//
// // key — это неэкспортируемый тип для ключей, определенных в этом пакете.
// // Это предотвращает конфликты с ключами, определенными в других пакетах.
// type key int
//
// // userKey — ключ для значений user.User в Contexts. Он
// // не экспортируется; клиенты используют user.NewContext и user.FromContext
// // вместо прямого использования этого ключа.
// var userKey key
//
// // NewContext возвращает новый Context, который несет значение u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext возвращает значение User, хранящееся в ctx, если оно есть.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key any) any
}
Context
несет в себе срок, сигнал отмены и другие значения через границы API.
Методы контекста могут вызываться несколькими goroutines
одновременно.
func Background
func Background() Context
Background
возвращает непустой Context
, не равный nil
. Он никогда не отменяется, не имеет значений и не имеет срока действия. Обычно он используется главной функцией, инициализацией и тестами, а также в качестве Context
верхнего уровня для входящих запросов.
func TODO
TODO
возвращает непустой Context
, не равный nil
. Код должен использовать context.TODO
, когда неясно, какой Context
использовать, или он еще не доступен (потому что окружающая функция еще не была расширена для приема параметра Context
).
func WithValue
func WithValue(parent Context, key, val any) Context
WithValue
возвращает производный контекст, который указывает на родительский Context
. В производном контексте значение, связанное с ключом, является val
.
Используйте контекстные значения только для данных в рамках запроса, которые проходят через процессы и API
, а не для передачи опциональных параметров функциям.
Предоставленный ключ должен быть сопоставимым и не должен быть типом string
или любым другим встроенным типом, чтобы избежать конфликтов между пакетами, использующими контекст. Пользователи WithValue
должны определять свои собственные типы для ключей. Чтобы избежать выделения памяти при присвоении interface {}
, ключи контекста часто имеют конкретный тип struct {}
. В качестве альтернативы статический тип экспортированных переменных ключей контекста должен быть указателем или интерфейсом.
Пример
Этот пример демонстрирует, как можно передать значение в контекст, а также как получить его, если оно существует.
package main
import (
"context"
"fmt"
)
func main() {
// Определяем пользовательский тип для ключей контекста
type favContextKey string
// Функция для поиска значения в контексте
lookupValue := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Printf("Found value for key '%s': %v\n", k, v)
return
}
fmt.Printf("Key '%s' not found in context\n", k)
}
// Создаем ключ и контекст со значением
languageKey := favContextKey("language")
ctx := context.WithValue(context.Background(), languageKey, "Go")
// Ищем существующее значение
lookupValue(ctx, languageKey)
// Ищем несуществующее значение
colorKey := favContextKey("color")
lookupValue(ctx, colorKey)
// Дополнительная проверка с другим типом (демонстрация безопасности)
otherKey := "otherKey" // обычная строка, не favContextKey
lookupValue(ctx, favContextKey(otherKey)) // конвертируем в правильный тип
}
Дополнительные рекомендации:
- Для production-кода:
// Лучше выносить ключи в package-level константы
const (
LanguageKey favContextKey = "language"
ColorKey favContextKey = "color"
)
- Добавить проверку типа значения:
if v, ok := ctx.Value(k).(string); ok {
fmt.Printf("Found string value: %s\n", v)
}
- Для сложных данных использовать указатели:
type configKey struct{}
ctx = context.WithValue(ctx, configKey{}, &MyConfig{...})
func WithoutCancel
func WithoutCancel(parent Context) Context
WithoutCancel
возвращает производный контекст, который указывает на родительский контекст и не отменяется при отмене родительского контекста. Возвращаемый контекст не возвращает Deadline
или Err
, а его канал Done
равен nil
. Вызов Cause
на возвращаемом контексте возвращает nil
.