I have now read
JEP 429 Extent-local variables (ELV), which is probably the most salient for the discussion about context.Context.
ELV allow to "bind" values a variable in a way that is local to a (virtual) thread. Those values are AIUI not always inherited to child threads, but they *are* inherited when using StructuredTaskScope. ELV are immutable, but it is possible to create a new binding, forming a sort of "stack". Using x.get(), you can get the value bound in the topmost frame of that stack.
This, of course, is exactly what context.WithValue/context.Context.Value does: It creates a stack of immutable bindings of values to "keys" (corresponding to the ExtentLocal instances) in a stack. Those bindings then propagate down the stack, but not up.
The design document makes one argument in favor of TLS/ELV in favor of explicit argument passing:
Normally, data is shared between caller and callee by passing it as method arguments, but this is not viable for a Principal shared between the server component and the data access component because the server component calls untrusted user code first. We need a better way to share data from the server component to the data access component than wiring it into a cascade of untrusted method invocations.
This, of course, is not a concern for context.Value. The key can simply be an unexported type, preserving the property that a context.Context does not give access to "private" state.
So, in effect, we can find a rough correspondence between these new Java concurrency primitives and Go:
- virtual threads correspond to goroutines
- structured concurrency correspond to errgroup and the cancellation-aspect of context.Context
- extent-local variables correspond to the value-aspect of context.Context
The one difference remaining is that Go requires the explicit passing of a context.Context, while Java stores both of its aspects ultimately in TLS.
AIUI, the Java side does not allow ELV to be inherited or passed to other threads, unless they are created as a sub-task using the structured concurrency primitive. Making that possible is exactly what Ian points out above as the argument in favor of explicit context passing. Basically, while I find it agreeable that structured concurrency is a good model, I'm unconvinced that it's the *only* model or the only model that should have access to Go's equivalent of ELV. As long as we want that, we have to pass context explicitly, ISTM.
Note that the decision to tie ELV to structured concurrency is very intentional. The section "Problems with thread-local variables" mentions three problems with TLS: Unconstrained mutability, Unbounded lifetime and Expensive inheritance. Of these, the latter two are solved by tying the ELV to structured concurrency. In Go, they are solved by explicit passing of context.Context.
There is one argument in that design doc in favor of ELV as a language environment construct in favor of the context.Context design: Performance. As ELV are supported implicitly by the compiler and runtime (that's the "Extent" in ELV), they can be much cheaper to access, as they don't need to walk the entire context stack. Instead (AIUI) the language environment provides a separate context.Context stack for each ELV.
This is very similar to what I described
a couple of years ago for local scoping in Go. Another way we might try to address this in the future is by collapsing context.Context transparently (i.e. have each context.Context own a map[any]any and store the values in there).
Again, I still don't see very strong arguments in favor of the Java ELV design over explicit passing. They explicitly decide not to allow the one thing that is argued in favor of argument passing - being able to inherit a context outside structured concurrency. I think that's a fine decision, I think restricting yourself to structured concurrency is a very defensible position to take - but I also don't think it's a decision Go necessarily has to follow.