Thanks for your examples!  I'll cover them in reverse order, to build up the syntax.

On 2/29/24 6:18 AM, 'TheDiveO' via golang-nuts wrote:

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):

   for {
        select {
            case <-sync.Canceled:
                break
            case <-...
        }
   }

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):

    context var sync.Canceled
    var cancelEvents func()
    sync.Canceled, cancelEvents = sync.CancelChan()

Obviously, this seems slightly awkward, but it's syntax and this can be fixed.  In fact as I argue on the latest version of the proposal on my github <https://github.com/samv/go-context-proposal>, that given that there needs to be a path for backwards compatibility.  It should be possible for new code to rely on `sync.Canceled` (or `sync.Cancelled`?) without requiring all callers to have moved away from context/. /Given there needs to be way to allow a regular call to `context.WithCancel()` to update the context variable in the caller, /and/ given that the "context register" has /callee-save /semantics, it should follow that functions that /don't/ restore the pointer could exist.  And given it's a property of the function, not the caller, that's where the marker should live.

I landed on `context func ScopeCanceler()`.  This is in line with  `context var variable SomeType`, and it's also a big warning up front that the function does something special, worthy of your understanding before you just go and call it.  It's also a hint to folk that it's a special type of function, not to be used everywhere like `ctx context.Context` was.  Of course, perhaps the marker ends up on the right with the return values, but like I say—it's syntax, and this can be decided later (especially if I got it wrong and this syntax makes parsing slower).

I expect we would end up with this:

   cancelFunc := sync.ScopeCanceler()
   defer cancelFunc()

While your test explicitly calls cancel() at the end and validates that everything shuts down (an excellent practice, often overlooked), many cases would be able to get away without the temporary:

   defer sync.ScopeCanceler()

If you were to jump-to-definition on `sync.ScopeCanceler()`, it might look like this:

   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)
            })
        }
   }

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.

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

--
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+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/002c3556-def6-4807-b613-cf453ae61faa%40vilain.net.

Reply via email to