22

How can I create a copy (a clone if you will) of a Go context that contains all of the values stored in the original, but does not get canceled when the original does?

It does seem like a valid use case to me. Say I have an http request and its context is canceled after the response is returned to a client and I need to run an async task in the end of this request in a separate goroutine that will most likely outlive the parent context.

func Handler(ctx context.Context) (interface{}, error) {
        result := doStuff(ctx)
        newContext := howDoICloneYou(ctx)
        go func() {
                doSomethingElse(newContext)
        }()
        return result
}

Can anyone advice how this is supposed to be done?

Of course I can keep track of all the values that may be put into the context, create a new background ctx and then just iterate through every possible value and copy... But that seems tedious and is hard to manage in a large codebase.

Nestor Sokil
  • 2,162
  • 12
  • 28
  • 4
    as long as you don't listen on context's done channel in the aync goroutine, you should be okay, right? – Saurav Prakash Jan 10 '19 at 12:48
  • 1
    You can implement your own `context.Context` interface implementation. – zdebra Jan 10 '19 at 12:49
  • @SauravPrakash Sure, what about the case when this async task is so complex that it has its own children context instances and you do listen if they're done? – Nestor Sokil Jan 10 '19 at 12:50
  • 2
    @NestorSokil then it should create its own new context and propagate that to child rather than using a possibly unrelated request context – Saurav Prakash Jan 10 '19 at 12:51
  • @SauravPrakash but it needs the values that were in the original context, how do I copy them? :) – Nestor Sokil Jan 10 '19 at 12:52
  • @zdebra and then I would have to cast it to perform a clone? Not a big deal, but I would still try to look for some simpler and cleaner solution... – Nestor Sokil Jan 10 '19 at 12:54
  • 1
    @NestorSokil any harm in manual copy of keys? – Saurav Prakash Jan 10 '19 at 13:03
  • If values are all you need, you can use `method value` to get more or less clear code `Value := ctx.Value` – Uvelichitel Jan 10 '19 at 13:12
  • @SauravPrakash no harm, I just don't know all of them. Or at least I don't have any way to restrict what gets added there. An unfortunate chain of events will always result in a situation where the required values are not available. – Nestor Sokil Jan 10 '19 at 13:28
  • @Uvelichitel there is no such method in context.Context interface – Nestor Sokil Jan 10 '19 at 13:29
  • 1
    @Nestor Sokil I mean https://play.golang.org/p/6WeRs-LMjh6 – Uvelichitel Jan 10 '19 at 13:45
  • @Uvelichitel oh, I see, that's pretty smart, will think about it – Nestor Sokil Jan 10 '19 at 14:18
  • One of many reasons why storing data in Context is generally a bad idea. – Adrian Jan 10 '19 at 14:27
  • @Adrian well it may be a bad idea, but it now also seems like an unfinished one. If the creators of context have implemented a way to add arbitrary values to it, then there should be a way to carry them over to a new context for such async tasks or other things. – Nestor Sokil Jan 10 '19 at 14:37
  • But they didn't, and there are other reasons Context is a terrible way to store data, so you might consider finding a better way to pass data around, like typed function arguments. – Adrian Jan 10 '19 at 15:08

3 Answers3

22

Update: Go 1.21 added WithoutCancel to the context package.


Since context.Context is an interface, you can simply create your own implementation that is never canceled:

import (
    "context"
    "time"
)

type noCancel struct {
    ctx context.Context
}

func (c noCancel) Deadline() (time.Time, bool)       { return time.Time{}, false }
func (c noCancel) Done() <-chan struct{}             { return nil }
func (c noCancel) Err() error                        { return nil }
func (c noCancel) Value(key interface{}) interface{} { return c.ctx.Value(key) }

// WithoutCancel returns a context that is never canceled.
func WithoutCancel(ctx context.Context) context.Context {
    return noCancel{ctx: ctx}
}
Peter
  • 29,454
  • 5
  • 48
  • 60
  • 2
    You'll have a big issue here if you're passing values in that context, because if the parent context is cancelled already, the child one might not cancel the operation, but will try to read the values from the parent context, which is probably already garbage-collected. – unmultimedio Jan 23 '20 at 22:48
  • 11
    As long as this context is around the parent isn't garbage, by definition, because there's still a reference to it. Accessing a garbage collected value is an oxymoron. – Peter Jan 24 '20 at 00:48
9

Can anyone advice how this is supposed to be done?

Yes. Don't do it.

If you need a different context, e.g. for your asynchronous background task then create a new context. Your incoming context and the one of your background task are unrelated and thus you must not try to reuse the incoming one.

If the unrelated new context needs some data from the original: Copy what you need and add what's new.

Scott Stensland
  • 26,870
  • 12
  • 93
  • 104
Volker
  • 40,468
  • 7
  • 81
  • 87
  • 1
    How about the values that are in the original context? The way the codebase at question works is that there are quite a few of those. And I don't want to track every case where a new context is created and investigate which values the downstream code may need. – Nestor Sokil Jan 10 '19 at 13:18
  • @NestorSokil Then a context.Context is not the appropiate transport mechanism für your data. You can either use a context and this includes cancelation and if you do not want to propagate cancelation than you _must_ make a new context. – Volker Jan 10 '19 at 14:02
  • I agree that the whole concept may be wrong, but as I said the codebase relies heavily on context. – Nestor Sokil Jan 10 '19 at 14:33
  • thank you for your input, but my specific problem should be solved with Peter's answer – Nestor Sokil Jan 10 '19 at 18:45
  • 2
    How about the case of an asynchronous http handler? After returning 202 and returning from the handler the request's context will shortly be cancelled. You likely need a context in the asynchronous goroutine which now does the work. If the server has middleware (eg. logging) that relies on writing and reading request scoped data to and from the context then the values from the request context may be needed. The handler itself certainly should not be made aware of all of these values (the internals of your middleware) so that it can copy them to a new context. – Bracken Sep 16 '21 at 10:20
  • 1
    Not all contexts are unrelated. I might want to trace a DB call but not cancel it if the parent context is canceled. – Alvaro Sep 06 '22 at 23:05
  • I agree this is a bad take, especially considering that `WithoutCancel` has just been added, for very valid reasons - some of which are mentioned in the comments above, to the standard library. – CAFxX Mar 30 '23 at 01:39
7

Starting in go 1.21 this functionality is available directly in the standard library via context.WithoutCancel:

func WithoutCancel(parent Context) Context

WithoutCancel returns a copy of parent that is not canceled when parent is canceled. The returned context returns no Deadline or Err, and its Done channel is nil. Calling Cause on the returned context returns nil.

CAFxX
  • 28,060
  • 6
  • 41
  • 66