Hi all,
Many moons ago I wrote a proposal to make execution context a fundamental concept in Go, effectively moving `context.Context` to being part of the language, not just an extension.
The main reason to do this is that then it would not be up to the source maintainer to have to do the refactoring it takes to retrofit `ctx context.Context` arguments passed to functions as the first position, as has become the common boilerplate. In some places, this is impossible: for instance, the io.Reader and related interfaces forbid a `context.Context` argument, so a reader cannot know where it is being read from.
Why does that matter, you might ask? The main reason is for
observability: tracing, logging, debugging, etc. Of course,
context should in general not be used to alter behavior, but for
observability, it seems like it would be fair game for the
append–only map that `Context`
provides, to be available even when working with less than stellar
code.
I'm wondering how people would feel about this now?
A summary of my original proposal would be to add a "soft"
keyword ("context") which is only
meaningful when used before "var",
but would not be prohibited from being a variable name. I did
some experimentation with the go parser and found I could get code
with 'context' added to is passing the parsing tests, so I'm
pretty sure this would work without slowing things down. That's
the syntax I'll use in this message, but of course this is subject
to feedback and taste.
Some basic principles stated as text for discussion; there are
code examples in the original proposal from August 2017.
Anyway I think that's the nutshell of it. Thoughts/questions/concerns?
Cheers, Sam
--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/24843040-a0f2-4054-8912-acf9defb7697%40vilain.net.
Hey Axel, thanks for the response.
The general reason this has not been accepted for Go, is that passing a Context explicitly removes ambiguities what is meant, in the presence of closures and goroutines.If you pass a closure to a different execution context (e.g. via a channel), it has two bindings for any dynamically scoped variable: One in the source stack and one in the destination stack. Neither of these seems more clearly correct than the other. Note, for example, that a callback could be called from a different goroutine altogether, yet close over the lexical scope of the function it's defined in.
It's an interesting question, however we don't need to use goroutines and channels to evoke this issue. Closures are enough. That is, if you make a closure in one function, save it somewhere, and then invoke it from a second location, then the closure has two potential scopes that could apply.
I would argue that the matter can be simply decided by choosing
the calling stack, not the destination stack. Choosing
the destination stack would mean that the closure no longer really
closes over its calling context. It is also possible, where
semantics require, to pass in specific context variables by value
to the closure, and that closure can set them in its immediate
(and any potential lower) scopes as needed.
If you want the opposite; context carries from the caller of the
func, instead return a bound method call. The bound method call
can have an identical type to a closure, and logically it is the
same thing, where the struct is the "closure" of state, but it
does not restore the context register when invoked. This seems
like a clear way to choose you want this func to use the destination
stack.
Goroutines also open some questions on how to efficiently implement this without races. To be fair, Context has the same issue, but by being a library type, it's used more sparingly, so the performance question is less critical.
I don't believe the data structure in Context presents any race conditions, and I also don't believe it's necessary for it to remain a deep linked list to retain that quality.
For instance, Context could create (static) map[any]any data structures for the subsequent contexts it spawns, eg it might decide to do this if the number of new contexts that have been created from it exceeds the "depth" to the next context with a "flattened" map by some threshold. So if it starts to get deep for a context that is creating a bunch of new contexts, it creates a flattened map and then switches an internal flag. This can be race–free. Would an implementation of this help move the needle on this, in your view?
Cheers & Happy Friday,
Sam.
I would argue that the matter can be simply decided by choosing the calling stack, not the destination stack.
Alex, I agree that there are cases where you might prefer one versus the other. However, you cut out the part of my reply where I pointed out it was possible to choose semantics by either returning a closure (context is the source stack) or a bound method (context is the destination stack). Both of these values can be used interchangeably, as they have the same type, func ..., and so the caller does not need to care whether the function they are calling uses the calling context or the original context. Were you not convinced by the argument?
Sam
Alright, well thanks for your input.
I do think these questions can be answered; exploring the use
cases in a proposal format should hopefully show that the impact
of closures would not normally be an issue. Possibly the worst
case is if you had a library to some external service, and at some
low layer you're using a closure, well you might need to refactor
that if you wanted to add cross–service tracing support. But to
be honest, it's probably better that the state you're depending on
is captured in a struct instead of being scattered about loosely
in a scope as with a closure. And the common practice in Go is to
return types that satisfy interfaces, not function references that
you blind call.
I think I will submit a proposal, but please don't take that to imply that I think you're "wrong, or mistaken". Your concerns are legitimate and the proposal should answer them cleanly. In the proposal format, hopefully the "litigation", or more exploration of possible uses and abuses of the system, along with performance concerns, can be addressed. I don't want to be dismissive of them, I just want to understand them.
I had a brief look on the Golang issues in Github and could not find any prior proposals along this line using "context" and "dynamic scope" as search terms, so I'll submit this as a "new" proposal for now.
Thanks again, and truly—thanks for responding, >100% better than people who just rolled eyes and marked thread as read.
Cheers,
Sam
-- Sam
On Feb 20, 2024, at 6:37 PM, Sam Vilain <s...@vilain.net> wrote:
--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/aec85a83-9497-469c-8369-2c9e60d0ca05%40vilain.net.
--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/aec85a83-9497-469c-8369-2c9e60d0ca05%40vilain.net.
On Feb 21, 2024, at 4:33 AM, 'Sean Liao' via golang-nuts <golan...@googlegroups.com> wrote:
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAGabyPpcxiexqPqmcFT-RNP8KZv5p8vUFw1%3DHfMwE87puCcGQg%40mail.gmail.com.
I had a brief look on the Golang issues in Github and could not find any prior proposals along this line using "context" and "dynamic scope" as search terms, so I'll submit this as a "new" proposal for now
Hey, thanks for raising this important use case. Do you have any specific examples in mind you could point to?
I think the answer should follow easily from what defines a new scope. New scopes get their own mini-stack frame created, and in that frame the context pointer can be saved like any other virtual register or 'g' property. Of course, you know that an if block has its own scope inside it, and variables declared inside the if blocks are not in scope outside of that block.
You can create a new scope without an if
statement, like this:
func ScopeTest() {
var varname = "outer"
{
var varname = "inner"
fmt.Printf("varname = %s\n", varname)
}
fmt.Printf("varname = %s\n", varname)
}
This prints:
varname = inner
varname = outer
It should follow that context variables declared in a block like
that would have their state unwound when exiting the block.
Of course, any change in the way that statements are written
brings with it change, resistance to change, and so on, so if you
feel like this is jumping through awkward hoops, of course I can
understand that position. I feel it myself when I consider
language changes a lot of the time. The way I like to look past
that is to make sure is that real-world examples don't end up contrived.
It should be clear what is going on when written in the way it
needs to be written to function as before, and that includes
concerns around backwards compatibility.
So back to the initial question: can you give an example of somewhere that a unit test uses context in a way that you predict this proposal will make problematic? I'd love to explore it to make sure, to make sure the proposal has lots of good examples.
Thanks,
Sam
--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/98dc6699-2b2e-48a8-bc4c-fc70406940e5n%40googlegroups.com.
-- Sam
The second example is unit test code, so TEST: https://github.com/thediveo/whalewatcher/blob/cca7f5676b3f63b0e2d6311a60ca3da2fd07ead7/watcher/moby/moby_test.go#L58
This one uses the `context.Done()`
/ `context.Err()` API:
ctx, cancel :=
context.WithCancel(ctx)
defer cancel()
In general, if you're re-assigning the new context to the same variable, then it can be directly replaced with setting the context variable. I reason that scope cancellation should be a sync property, so you would write (in the cancelable function):
OK? So, assuming that, then it follows that setting context var sync.Canceled (and watching it) is the way to make the current scope cancelable (using the previous described syntax):for {
select {
case <-sync.Canceled:
break
case <-...
}
}
cancelFunc := sync.ScopeCanceler()defer cancelFunc()
defer sync.ScopeCanceler()
Remember, this is syntax, not semantics, and we need to nail down the semantics first, but this is the working syntax I'm using for the proposal.type CancelFunc func()type CancelChan <-chan struct{}
context func ScopeCanceler() CancelFunc {var cancelChan = make(chan struct{})context var Canceled CancelChan = cancelChan
var guard Once
return func() {guard.Do(func() {
close(cancelChan)})
}
}
My first example is code that is run in PROD: https://github.com/thediveo/whalewatcher/blob/cca7f5676b3f63b0e2d6311a60ca3da2fd07ead7/watcher/watcher.go#L240
So, building on the earlier example, you're asking about this
block:
eventsctx, cancelevents :=
context.WithCancel(ctx)
evs, errs :=
ww.engine.LifecycleEvents(eventsctx)
Here the difference is that you're assigning the context to a different variable, meaning you can choose which of the variables you use in a given line.
That said, your example happens to do something also quite common: it creates a new context, passes it to a specific function, and then does not use it again. All this means is that you need to create a new scope, which could be a method, closure, or an explicit scope block, and assign the variables you want to use later to variables that are in the outer scope, and call the function you want to inherit the new context from inside the scope.
For example, using an explicit scope:
var cancelevents func()
var evs <-chan ContainerEvent
var errs <-chan error
{
cancelevents = sync.ScopeCanceler()
evs, errs = ww.engine.LifecycleEvents()
}
That might look like a lot of annoying variable declarations, and
that you'd rather take advantage of type inference. But if you
were to put the code into a method, then you can then use a
type-inferring assignment statement, and that would be a more
likely and natural refactoring approach for that code.
Hopefully these two examples might shed some more light for you on a certain class of usages.
Yes, of course! Thanks for your input.
Of course there are contrived cases where multiple contexts are
passed to a function, or a function really does a lot of juggling
of different contexts for use by different functions. In the
cases I've looked at, I have found that there exists a refactoring
that aligns the use of the context with a function, and that the
resulting code is cleaner.
Cheers,
Sam