Умение работать с пакетом context является очень важным: его использование в Golang пронизывает весь код, но не смотря на это очень часто он используется формально. Контекст создается (иногда непосредственно перед вызовом функции), передается из одной функции в другую и дальше по цепочке. Для чего это делается, в чем конечная цель? Для того, чтобы ответить на этот вопрос необходимо сделать шаг назад и опереться на знания о каналах, горутинах и шаблонах работы с ними.
При программировании на go, мы повсеместно создаем горутины, которые запускают дочернии горутины, те в свою очередь также могут запускать горутины и так далее. Несмотря на то, что горутины легковесны, ресурсы имеют свойство заканчиваться (goroutine leaks), в связи с чем возникает необходимость прекращать их работу. В go для коммуникации между горутинами используются каналы. Для отправки сигнала на прекращение работы горутины используется канал, обычно именуемый done и передаваемый в качестве первого параметра горутины:
done := make(chan struct{})
// отложенное закрытие канала
defer close(done)
go func(done chan struct{}){
for {
select {
case <-done: return // прекращаем работу горутины
case: ...// здесь логика
}
}
}(done)
....
<-done
Таким образом, код запускающий горутину получает возможность завершить ее выполнение, не допуская утечки (gorutine leak). Существует потребность в разных способах прекращения работы горутины: просто отмена (cancel), завершение работы по истечении времени (cancel with timeout), прекращение в какой-то момент времени (cancel with deadline) и другие сценарии, имплементацию которых предоставляет пакет context, тип которого context.Context является оберткой над каналом done. Да, именно так: в типе context.Context есть метод: Done() <-chan struct{}, который возвращает канал, закрывающийся, когда выполнение горутины должно быть прекращено.
Но давайте по порядку. Мы собираемся продемонстрировать основные приемы работы с пакетом context, параллельно рассказывая, каким образом тот же функционал реализовать, используя канал done.
Создание контекста
ctx := context.Background()
Так мы создаем базовый контекст. Базовый контекст мы не будем передавать в дочерние горутины, причину рассмотрим ниже. Также создать контекст можно следующим образом:
ctx := context.TODO()
Последний способ используется реже, в тех случаях, когда не ясно какой контекст использовать или он пока недоступен.
Важно упомянуть метод типа context.Context: Err() error, который возвращает nil, если канал Done не закрыт, ошибку Canceled или DeadlineExceeded в зависимости от того, какой тип отмены контекста произошел. Подробнее об этих ошибках расскажем при рассмотрении функций: WithCancel, WithTimeout, WithDeadline.
Итак, место канала done занимает контекст, буквально: общепринятая практика передачи контекста в качестве первого аргумента функции (горутины):
func calculateCtxVersion(ctx context.Context) {
for {
select {
case <-ctx.Done(): return
case: ...// здесь логика
}
}
}
Для сравнения приведу функцию, использующую канал done для коммуникации с дочерней горутиной:
func calculateChannelVersion(done chan struct{}){
for {
select {
case <-done: return
case: ...// здесь логика
}
}
}
Пока все выглядит очень похоже, на правда ли? Но чего то не хватает.
Для отправки сигнала на закрытие горутины через done мы закрываем канал: close(done).
Каким образом послать сигнал в случае использования контекста? Базовый контекст мы создали, но способов его отмены у нас нет.
Отмена контекста
Возможность отмены контекста предоставляет следующая функция из пакета context:
func WithCancel(parent Context)(ctx Context, cancel CancelFunc)
WithCancel возвращает копию родительского контекста (parent) c новым каналом Done, который закрывается вызовом функции cancel (метод Err() вернет ошибку Cancelled) или когда канал Done родительского контекста закрыт, в зависимости от того что случится раньше.
// создаем базовый контекст
ctx := context.Background()
// создаем копию контекста
ctx, cancel := context.WithCancel(ctx)
// отложенный вызов отмены контекста
// закрытие канала Done этого контекста
defer cancel()
go calculateCtxVersion(ctx)
...
<-ctx.Done()
Без использование контекста:
done := make(chan struct{})
defer close(done)
go calculateChannelVersion(done)
...
<-done
Отменить контекст можно по истечению определенного времени (timeout):
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout возвращает копию родительского контекста (parent) с новым каналом Done, который будет закрыт через интервал времени, указанный в timeout (метод Err() вернет ошибку DeadlineExceeded) или в случае вызова возвращенной функции cancel или когда будет закрыт родительский канал Done, в зависимости от того что случится раньше.
timeout := 5 * time.Second
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
go calculateCtxVersion(ctx)
...
<-ctx.Done
Без использование контекста:
done := make(chan struct{})
defer close(done)
go calculateChannelVersion(done, timeout)
...
<-done
В функции calculateChannelVersion нужно внести изменения в области оператора select:
select {
case <-done: return
case time.After(timeout): return
case: ...// здесь какая-то логика
}
Отменить контекста по достижению определенного момента времени. Тут все очень похоже на WithTimeout:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDealine возвращает копию родительского контекста (parent) с новым каналом Done, который будет закрыт в момент времени, указанный в d или в случае вызова возвращенной функции cancel (метод Err() вернет ошибку DeadlineExceeded) или когда будет закрыт родительский канал Done, в зависимости от того что случится раньше.
ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, timeInThefuture)
defer cancel()
go calculateCtxVersion(ctx)
...
<-ctx.Done()
Без использование контекста:
// вычисляем timeout
timeoutInterval := time.Until(timeInTheFuture)
// дальше так же как в предыдущем примере
Контекст без отмены
Интересный функционал предоставляет функция
func WithoutCancel(parent Context) Context
WithoutCancel возвращает копию родительского контекста, которая не отменяется в случае отмены родительского контекста. Создание контекста, который не будет отменен не смотря на отмену родительского контекста, может быть полезно при использовании (передаче) этого контекста в критически важных операциях, выполнение которых не должно быть прекращено даже при отмене родительского контекста, возможно, для поддержания консистентности данных или других важных операций, выполнение которых должно быть продолжено.
Возникает резонный вопрос: почему бы не создать новый контекст. Но этот вариант не подойдет для тех случаев когда контекст содержит важную для программы информацию.
Причина отмены
Рассмотрим семейство функций WithCancelCause, WithTimeoutCause, WithDeadlineCause на примере функции WithCancelCause. Они предоставляют расширенный функционал, позволяющий работать (указывать и получать) причину (cause) отмены контекста:
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
WithCancelCause ведет себя так же как и WithCancel, но возвращает функцию типа CancelCauseFunc вместо CancelFunc, вызов которой с ненулевой ошибкой в качестве параметра, записывает эту ошибку в контекст («cause» — причина).Эта ошибка может быть получена при помощи вызова Cause (ctx):
ctx := context.Background()
ctx, cancel := context.WithCancelCause(ctx)
defer cancel()
...
// останавливаем работу горутины и записываем причину
cancel(errors.New("context cancelled"))
...
// получаем причину отмены контекста
err := context.Cause(ctx)
После отмены
Завершим обзор пакета context функцией:
func AfterFunc(ctx Context, f func()) (stop func() bool)
Организует вызов функции f в собственной горутине после завершения контекста (отмены или истечения времени ожидания). Если контекст (ctx) уже выполнен, AfterFunc немедленно вызывает функцию f в собственной goroutine. Вызов функции stop отвязывает функцию f от контекста ctx:
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
stop := context.AfterFunc(ctx, func() {
…
})
Заключение
Мы рассмотрели значительную часть функционала, предоставляемого пакетом context, что надеюсь позволит использовать его максимально эффективно. В одной из следующих статей мы продолжим изучать контекст и его роль в организации gracefull shutdown.