При использовании пакета «context» в Go проверка «lostcancel» команды go vet
выдаст предупреждение, если вы не вызовете функцию отмены, возвращаемую context.WithCancel(). например если у вас есть:
func main() {
ctx, cancel := context.WithCancel(context.Background())
foo(ctx)
fmt.Println("done")
}
...тогда Go vet предупредит вас:
функция отмены используется не на всех путях (возможна утечка контекста)
Это также упоминается в документации пакета Context:
Если не удается вызвать CancelFunc, происходит утечка дочернего и его дочерних элементов до тех пор, пока родитель не будет отменен или не сработает таймер. Инструмент go vet проверяет, используются ли функции CancelFunc на всех путях потока управления.
Моя проблема заключается в следующем: по крайней мере в одном случае я не понимаю, почему должна вызываться функция отмены. Предположим, я хочу получить дочерний контекст, чтобы у меня была возможность:
Мне кажется, это очень естественный вариант использования древовидной структуры родительского/дочернего пакета контекста. Но это дает ту же ошибку go vet
, что и упомянутая выше, потому что в пути выполнения кода, представляющем второй вариант (позволить дочернему элементу жить в течение той же продолжительности, что и родительский), функция отмены дочернего контекста не будет вызываться.
Итак, действительно ли это недостаток go vet
/рекомендаций сообщества Go по использованию контекста, или это ошибка в моих размышлениях на эту тему?
Вот пример программы, в которой возникает ошибка. Go vet вынесет предупреждение за линию barctx, barcancel := context.WithCancel(ctx)
. Этот пример явно глупый, потому что я мог бы просто проверить bar
на наличие ошибки перед запуском горутины. Но достаточно сказать: в моей реальной программе это было бы невозможно.
func main() {
ctx, cancel := context.WithCancel(context.Background())
foo(ctx)
cancel()
time.Sleep(10 * time.Millisecond)
fmt.Println("done")
}
func foo(ctx context.Context) {
go func() {
<-ctx.Done()
fmt.Println("foo done")
}()
for i := 0; i < 2; i++ {
barctx, barcancel := context.WithCancel(ctx)
err := bar(barctx, i)
if err != nil {
fmt.Println("bar error; canceled")
barcancel()
}
}
}
func bar(ctx context.Context, x int) (err error) {
go func() {
<-ctx.Done()
fmt.Printf("bar done; x=%d\n", x)
}()
if x > 0 {
err = fmt.Errorf("bar error")
}
return err
}
В вашем примере go vet
было бы правильно вернуть предупреждение. Проблема конкретно заключается в функции bar
. Когда вы его создаете, bar
создает горутину, которая блокируется до тех пор, пока не будет вызвана barcancel
или cancel
. До тех пор он будет продолжать существовать. Поэтому крайне важно вызвать функцию отмены, чтобы ресурсы, ожидающие выполнения контекста, могли выйти за пределы области действия и быть удалены сборщиком мусора.
Чтобы было ясно, в вашем примере все отложенные подпрограммы будут отменены, потому что вы отменили ctx
, который также отменил каждый экземпляр barctx
. Однако вы не должны зависеть от этого, поскольку вы можете не контролировать, будет ли отменен родительский контекст. Итак, ваша функция должна взять на себя ответственность за контексты, которые она создает и использует при вызовах функций.
Чтобы было ясно, вы должны изменить свой примерный код следующим образом:
barctx, barcancel := context.WithCancel(ctx)
err := bar(barctx, i)
if err != nil {
fmt.Println("bar error; canceled")
}
barcancel()
@ tbell5 Я понимаю твою точку зрения. В вашем примере вы пытаетесь создать функцию «установил и забыл», которая отменяется только при возникновении ошибки или отмене родительского контекста?
«Когда вы его создаете, bar создает горутину, которая блокируется до тех пор, пока не будет вызвана barcancel. До тех пор она будет продолжать существовать, даже если ваша основная функция завершится». Это не верно.
@ Woody1193 Да, верно. Но теперь я вижу, что не использую дочерний контекст по назначению. Я думаю, что у меня просто плохо спроектированный код, и, вероятно, его следует просто реорганизовать.
@Volker После дальнейшего изучения я вижу, что вы правы. Спасибо, что просветили меня
Но в этом примере я не хочу, чтобы горутина в
bar()
была отменена, если она не вернет ошибку. Как я уже сказал, я хочу, чтобы он жил так же долго, как родитель. Это правда, что я делаю предположение, что у меня есть контроль над родителем, но это также верно в этом случае. Это может быть правдой во многих случаях. Го ветеринар мог ясно видеть, что это правда. Но я все равно понимаю вашу точку зрения, я думаю, я просто не использую контекст так, как его предполагалось использовать.