
An interesting mistake with Go's context package - pcr910303
https://utcc.utoronto.ca/~cks/space/blog/programming/GoContextValueMistake
======
cfors
Go's context package is great, but also hard to reason about. As developers,
we often times like to think that just piping a context through a function
chain will take care of all of your timeouts concerns for you.

Some fun context gotchas:

\- Using `req, err := http.NewRequest(...)` to create an http request, and
then calling `client.Do(req)` does not do anything with context, you need to
use `http.NewRequestWithContext(...)`. [0]

\- Creating a function that accepts a `ctx context.Context` and then making a
blocking call that doesn't respect your context will still block for the
entirety unless you instrument it yourself.

\- If a parent context is canceled, all children (derived) contexts will also
be canceled. However, only if they are explicitly checking `context.Done()`.

\- And the worst of all is passing things that decide business logic in your
context variables! First, you lose all benefits of go's static typing. Second,
you will not realize all of the crazy places that a context scoped variable is
coming from, have fun debugging that!

[0]
[https://golang.org/pkg/net/http/#NewRequest](https://golang.org/pkg/net/http/#NewRequest)

~~~
bigdubs
The issue here is contexts were introduced after the API for
`http.NewRequest()` was guaranteed to be stable, so there are in effect (2)
different versions of the api, one without contexts and one with.

IMHO it's a rock and a hard place; either you break compatibility, or you have
a slightly more confusing API.

~~~
erik_seaberg
Or you add first-class context handling to the language, because this won't be
the last time and continually adding more plumbing to every method signature
is unsustainable.

~~~
masklinn
What would "first-class context handling" be though, dynamic scoping?

~~~
kevincox
Probably.

I know dynamic scoping is considered dirty but I'm not convinced that it
doesn't have its place. I think the problem in the past is that dynamic
scoping often replaced syntactic scoping. But it can be a nice replacement for
when you are deciding between a global or a context parameter to every
function.

~~~
erik_seaberg
I'm thinking something like panicking all the goroutines started in a context
that's been cancelled, and something like defer to handle cleanup during
cancellation. Some expression that can read and write a typed value for a key
in the current context (not even the standard library can do this without
generics). If contexts have to become part of the calling convention, that's
fine, just don't add more noise to every function call in the source.

------
throw_m239339
If the Go team made Goroutines types like ADA tasks, nobody would have to deal
with any of the problems people using Goroutines have to deal with. This is
yet another short sighted design decision made by Go designers. There would be
no need for a context package at first place. The Go team tried once again to
solve a language design problem with a library, it didn't work.

~~~
masklinn
Could you explain how Ada tasks work and why they solve the issues the context
package is intended for, for those who don't know ada?

~~~
johnisgood
I do not know if this is anything close to what you want, but:

[https://news.ycombinator.com/item?id=19245898](https://news.ycombinator.com/item?id=19245898)

and

[https://news.ycombinator.com/item?id=23609499](https://news.ycombinator.com/item?id=23609499)

might be of use.

------
earthboundkid
Chris’s blog is general quite good, but I thought this entry was weak. I guess
he doesn’t use context much for his devops stuff.

If you use context at all, you know that it doesn’t mutate in place. That’s
why it’s goroutine safe. It’s the whole point of using context. The answer to
the pop quiz was obvious, and I only second guessed myself because I thought
it must be a trick question somehow. Like “well obviously you can’t mutate a
context but maybe context.TODO is broken somehow”.

------
mperham
The Context package is tricky to use correctly. I got bit because I designed
an API that assumed you could embed a Context within a larger struct. Because
it's immutable you can't and have to pass it as a value everywhere. It wasn't
fun having to break backwards compatibility to fix that mistake.

~~~
AlexCoventry
Not sure what you mean by "embed a Context within a larger struct." It's
certainly possible to have a struct field of type context.Context.

~~~
mperham
I didn’t say it wasn’t possible, I meant could as in “safe to do so”. Read the
package docs, it explicitly says not to do that.

~~~
morelisp
It says not to, but the reason is because you will probably not handle context
semantics correctly, not because it inherently breaks anything.

If the object you embed the context in has all the properties of a context
(short-lived, request-scoped, passed to every function that needs to handle
it, and provides a way to create a nested context) then embedding the context
is at worst unidiomatic. This is how http.Request did it to preserve
backwards-compatibility; http.Handler usage was too pervasive to ever change
its signature.

------
shirro
For a language which seems to have been intended initially for writing
maintainable network services the absence of a means to set timeouts or cancel
requests in the original network apis was clearly an oversight. I like what
they have proposed with generics and other recent changes and I would like to
see a fresh look at this problem.

Context is working as intended here as it returns a copy which is well
documented. Cancellation and timeouts are useful but WithValue seems like a
nasty hack to me. I think it should be avoided and possibly deprecated.

~~~
dvt
This is actually kind of funny (and true) as I was the one that implemented
sensible HTTP timeouts in the standard library after a server I wrote oddly
kept eternally open-connections[1].

[1]
[https://github.com/golang/go/issues/213](https://github.com/golang/go/issues/213)

------
sudhirj
One of the things I miss in Go is immutability. Here is the context object is
trying to be kept immutable with the pattern of returning a copy of the object
with the mutation, which is pretty great but needs to be popular in the
ecosystem to use it effectively.

~~~
cle
It’s not about immutability, it’s about cascading contexts, all the Context
“With” functions create children that are automatically cancelled when any
ancestor is cancelled.

~~~
morelisp
It is about immutability, at least in the visible API. The stdlib context
functions all only expose the `context.Context` interface which has no methods
to mutate it. Any "changes" to a context can only be done by making a child
context, which also ensures the caller's desired deadline/timeout semantics
are enforced as each layer can only add cancellation conditions; similarly you
cannot remove values, only replace them.

(Though internally there are some nasty optimizations to chain cancellation
with minimal channel waits.)

------
gravypod
Very interesting article. I've run into similar issues with Contexts (in gRPC
+ Java). It's been so bad they I've had to hide all interaction with contexts
behind interfaces that only allow down stream users to call `getThing()` to
pull things out of the contexts.

Contexts seem like exceptions but backwards and with less syntactic sugar
which feels like an extremely annoying design clash with golang's error
handling semantics. It seems like a better way of handling this would be some
dynamic thread/context aware DI system but that's also not very go-y.

Does anyone have experience taming complexity here around this area?

~~~
georgethomas
I've experienced both: explicit passing of immutable Contexts in Go and using
ThreadLocal storage in Java (DropWizard) apps.

I think it was our mistake to use ThreadLocal storage for request-specific
data. This directly couples threads (which are an OS / scheduling concern) to
the serving of requests. We then ran into issues spawning new background
threads, or naively switching to Kotlin Coroutines (which can be scheduled on
a different thread).

So I prefer the explicit, immutable context passing that Go uses. The
`getThing()` pattern you mention seems reasonable and gives you some type
safety that is otherwise missing with a bare `context.Value()` calls.

~~~
gravypod
I think golang is in a unique position to make `ThreadLocal`-like
communication-by-state a workable solution due to the go-routine process
model. You can get the benefits of imitable-ness + type safety of
`ThreadLocal`-ness with some simple maneuvering.

This could be accomplished by treating "context" like in-process environment
variables. Essentially:

1\. My Threadlet Variables are immutable to me.

2\. I can create children Threadlets (by default) inherit all of my Threadlet
Variables.

3\. I can modify the Threadlet Variables of my children Threadlets.

This could look something like this (Java + Golang mashup):

    
    
        class User {
            ...
        }
    
        class UserSingleton implements ThreadletLocal {
             // implements static set() and static get() and reset() or something
        }
    
        requestMiddleware(request, next) {
            go next(request), Context.with(
               UserSingleton.set(extractUserFrom(request.headers)),
               DeadlineThreadletLocal.within(1 second)
            );
        }
    
    
    

This idea is much more refined and further developed in Laravel's facade APIs.
They can bind an object dynamically to a "class" and change that underlying
implementation dynamically. The benefit of this approach is you can actively
distinguish between: 1. who is _using_ your the User class and 2. who is
_injecting /receiving_ a User class statically. This allows you to
automatically track your "DI". You could also make some syntactic sugar for
this like:

    
    
        class UserSettingsController {
           @ProvidedBy(UserSingleton.class)
           Optional<User> user;
    
           .... some code here who talks to user ....
        }
    
    

From a Java perspective I think this should be possible post-Loom.

------
shorsher
Here's a question from someone who is recently out of school and somewhat new
to Go. Is it OK to use Context for request-scoped values? i.e. passing data
between middleware? I have started doing this, but is there a better
alternative?

Peter Bourgon's blog post[0] about context made me think it is fine. After
reading the comments here, I am not so sure. Especially since his post was
roughly 4 years ago.

0:
[https://peter.bourgon.org/blog/2016/07/11/context.html](https://peter.bourgon.org/blog/2016/07/11/context.html)

~~~
ChrisFoster
I liked the following blog post where Jack Lindamood argues pretty
convincingly against using Context.Value in nearly all circumstances, and
gives some nice alternatives.

[https://medium.com/@cep21/how-to-correctly-use-context-
conte...](https://medium.com/@cep21/how-to-correctly-use-context-context-in-
go-1-7-8f2c0fafdf39)

------
zmj
Aren't contexts supposed to flow mutations down the call stack, not up it?

~~~
nkozyra
That was my first impression and why I guessed nil value.

~~~
eternalban
And if the functions in question were properly named

    
    
       func NewWithValue(parent Context, key, val interface{}) Context
    
       func NewBackground() Context
    

you wouldn't even have to guess.

~~~
masklinn
IDK, having never seen or used Go's contexts, the `With` prefix immediately
made me think that it likely created a new context: it's a common naming
convention in Rust or Python.

I'd have expected `SetValue` for updating keys in-place.

That `WithValue` creates an "inheritance" relationship surprised me a lot
more, _that_ I did not expect.

------
Nullabillity
At the risk of sounding like a broken record, Rust attacks this problem from
multiple avenues...

1\. You can tag a type or function as #[must_use], either of which makes
discarding a value trigger a warning. This would have caught the (equivalent
of the) example code.

2\. Futures provide pretty much everything that Context does, but without
having to thread a magical value correctly through everything. Explicit
cancellation can be had just by.. dropping the Future. This means that you can
get timeouts by racing with a timer Future. Arbitrary values (as in the
example) are slightly more verbose, but you can have a decorator Future that
sets a thread-local for the duration of the poll.

~~~
papaf
_Explicit cancellation can be had just by.. dropping the Future._

Its great to have a Rust expert to turn up in a Go thread to educate the
masses.

I have one question from the cheap seats though. How do you drop the Future,
with a decision made at runtime, given that lifetimes are part of the type
system?

Thanks for your help.

~~~
msbarnett
> I have one question from the cheap seats though. How do you drop the Future,
> with a decision made at runtime, given that lifetimes are part of the type
> system?

Trivially, something like

    
    
        let future = some_long_running_async_function();
        let timer = delay(Duration::from_millis(10000));
    
        select! {
            result = future => {
              // handle result of future completing successfully
            },
            _ = timer => {
                // future missed the deadline, drop it, terminating execution.
                std::mem::drop(future);
            }
        }
    

If this were wrapped in a function you could also just return out of the timer
branch and let the future naturally drop as the stack popped.

