Why context.Context not in structs ?

4,384 views
Skip to first unread message

foxnet.d...@googlemail.com

unread,
Aug 13, 2015, 9:48:59 AM8/13/15
to golang-nuts
Hi,

context.Context is a great package, with die docs says:

Do not store Contexts inside a struct type;
instead, pass a Context explicitly to each function that needs it.

But why ? I often have situations, where storing a context is really useful.
Especially when dealing with derived io.Writer/Reader implementations.

Like NewReader(ctx, ...). The Read/Write function obviously do not allow passing a ctx...
You might ask, why the hell does he need a ctx in a Read/Write method. But there are useful cases ;).

If I'm not allowed to store a context in a struct, can I at least store the .Done() channel in a struct ?


Shawn Milochik

unread,
Aug 13, 2015, 10:22:53 AM8/13/15
to golan...@googlegroups.com
It's more explicit. If you make it part of the function definition, you require every call to pass a context.

It's easier; you can use structs from third-party packages naturally, without having to embed to add a context.

I don't know your "done channel" answer.

Sameer Ajmani

unread,
Aug 13, 2015, 12:00:49 PM8/13/15
to Sh...@milochik.com, golang-nuts
Satisfying the interface io.Reader or io.Writer is a good reason to put a Context in a struct, but in general this should be avoided.  The reason to avoid putting Contexts in structs is that Contexts should follow the synchronous (blocking) call graph of your program: this is what allows the cancelation behavior to be meaningful (canceling a Context should cancel the blocking calls running on behalf of that Context).  Putting a Context in a struct (typically) means that the methods on that struct use that Context.  This causes problems when those methods are called from functions in a different Context:

type S struct {
  ctx Context
}
func (s *S) Foo() {
  bar(ctx)
}

func Baz(ctx context.Context, s *S) {
  s.Foo()  // uses s.ctx, not ctx
}
This call to s.Foo won't be canceled when Baz's ctx is canceled.  It also won't use any security credentials from Baz's ctx.  It also won't propagate any trace IDs or loggers from Baz's ctx.  The right way to do this is for Foo to take a context:
func (s *S) Foo(ctx context.Context) {
  bar(ctx)
}
func Baz(ctx context.Context, s *S) {
  s.Foo(ctx)
}

However, in the specific case of satisfying an interface like io.Reader or io.Writer, storing the context in a struct is appropriate. But to avoid issues like the example above, this binding to ctx should be done locally to the use.  One idiom we use in Google is to name the method "IO":
func (s *S) IO(ctx context.Context) io.ReadWriter {
  // return a struct that implements Read and Write on s using ctx
}

Then code can use s.IO(ctx) as an io.Reader or io.Writer:
func Baz(ctx context.Context, s *S) {
  fmt.Fprintln(s.IO(ctx), "hello, world")
}

We've tried and failed to document the rules around this, because it's really an API design judgement.  The general rule is to avoid putting Contexts in structs and make them follow the blocking call graph.

S


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

foxnet.d...@googlemail.com

unread,
Aug 14, 2015, 5:54:14 AM8/14/15
to golang-nuts, Sh...@milochik.com
Alright, got it =).
Thanks!

foxnet.d...@googlemail.com

unread,
Aug 14, 2015, 2:56:53 PM8/14/15
to golang-nuts, Sh...@milochik.com
Hi, I just thought about an example, where storing a Context in a struct actually does make sense:

Imagine a NewXXX function, which takes a ctx and returns some struct, lets call it TaskRunner.

It also launches a goroutine in the background, which is necessary for the struct to operate (maybe a task poller or whatever).
The struct also has a method, lets call it offerTask(localCtx, ... task), which blocks, while the task is processed.
Due to blocking, it also takes a context.

Now, a running and blocking offerTask should return, when
- localCtx is cancelled
- the ctx passed to NewXXX is cancelled (because the background goroutine is obviously not able to accept tasks after cancelling).
- the task succeeds.

The Select should look like this:

select {
case <-localCtx.Done():
// clean up
return
case <-TaskRunner.ctx.Done():
// clean up
return
case <-taskResultChan:
fetch result...
}

Since the TaskRunner.ctx is local to NewXXX, it cannot be used in offerTask, without storing it in a struct, but this use case does not have any of your cons, since it also takes a localCtx.

In my opinion, this is a perfectly valid and suitable use case for storing ctx in structs. (As long you make sure to select on all ctx's).

What do you think about this ?

Thanks in advance for this interesting discussion =).

Regards,
Chris

PS: 
If you are interested: 

My use case is a token bucket, with a goroutine running and generating tokens.
I want to shutdown the token bucket, when a specific ctx is cancelled NewTokenBucket(ctx...).
There is a method called Take(ctx), which tries to take tokens and blocks if no tokens are available.

Now, Take(...) should unblock, if the bucket is cancelled, because it would otherwise block forever.

Right now, I do this with a manual done channel, but I think its wrong to duplicate exit strategies.


Am Donnerstag, 13. August 2015 18:00:49 UTC+2 schrieb Sameer Ajmani:

Sameer Ajmani

unread,
Aug 15, 2015, 6:16:19 PM8/15/15
to foxnet.d...@googlemail.com, golang-nuts, Sh...@milochik.com

Thanks for the example.  In this case, the ctx passed to New is only being used for its Done channel; none of the request scoped values matter. So New doesn't need a ctx, it just needs a done channel.  But even that is unusual: much more common is for the new object to have a Close method that shuts down the Background goroutine. Internally, this shut down mechanism can use a channel.

Reply all
Reply to author
Forward
0 new messages