Many have written about this; there's an index on Go wiki under 
ExperienceReports#context 
<https://github.com/golang/go/wiki/ExperienceReports#context> including my 
own writing for the 2016 advent calendar.

I would like to propose a simpler way to solve the "optional variable 
access" part of context as a language feature.  The core problem I'm trying 
to solve is the necessity for large scale refactoring to add the ctx 
variable to every caller in a stack.  I've done this before for the purpose 
of adding log tagging, tracing, cancellation, and database 
transactions/savepoints management.  I believe it is a good fit for those 
cases.  I've also seen bad usages of it - people shoehorning required 
variables into it to escape the difficulties of rethinking their design.

The basics:

   - the runtime adds an extra stack argument for its equivalent of the 
   Context structure.
      - this argument is not directly accessible
      - it may be eliminated/optimized out by the compiler where not used
      - it may be "unrolled" to more than one stack argument for performance
   - a special keyword added for declaring/accessing 'context variables'
   - assigning to a context variable creates a new context
   - may make cancelation easy and natural

*Setting context variables*

So, for example, this use of context.Context:

type myPrivateType struct{}
var myPrivateKey myPrivateType

func someFunc(ctx context.Context, args... interface{}) {
    localInfo := &LocalInfo{}
    ctx := context.WithValue(ctx, myPrivateKey, localInfo)
    ...
}


Would instead be:

func someFunc(args... interface{}) {
    context var localInfo := &LocalInfo{}
    ...
}

 
*Retrieving context variables*

*Retrieving* a value would change from:
 

func otherFunc(ctx context.Context, args... interface{}) {
    localInfo, ok := ctx.Value(myPrivateKey).(*LocalInfo)
    ...
}

To:

 func otherFunc(args... interface{}) {
     context var localInfo *LocalInfo
     ...
}


*Mid-function variable re-assignment*

Re-assigning a previously declared variable would have the same effect as 
re-assigning a ctx variable in scope:

func someFunc(ctx context.Context, args... interface{}) {
    localInfo, ok := ctx.Value(myPrivateKey).(*LocalInfo)
    ... 
    if somecondition {
        ctx = context.WithValue(ctx, myPrivateKey, localInfo)
    }
    ...
}


Would be equivalent to:

func someFunc(ctx context.Context, args... interface{}) {
    context var localInfo *LocalInfo
    ... 
    if somecondition {
        localInfo = &LocalInfo{}
    }
    ...
}


I wouldn't treat this exact equivalence as a hard rule; this re-assigning 
would only be expected to affect that particular context variable. 

*Public context variables*

It could also be possible to access other packages' public context 
variables:

func someFunc(args... interface{}) {
    context var pkg.FooVariable otherPkg.SomeType 
    ...
}

This declaration is a little weird, because it includes a package name in 
the variable name.  The immediate question is, is this variable accessed 
later as "pkg.FooVariable" or just "FooVariable"?  I would lean towards the 
former to be less surprising and to avoid potential namespace clashes.

This could be useful for log tagging; APIs would look like;

    context var logging.Tags []zap.Field
    logging.Tags = append(logging.Tags, zap.String("rqID", rqUUID))

Where logging is some project-global logging module.

This isn't quite the pattern I used in my context logging post, which was:

logging module:

    // WithRqId returns a context which knows its request ID
    func WithRqId(ctx context.Context, rqId string) context.Context {
        return context.WithValue(ctx, requestIdKey, requestId)
    }


calling package:

    func RequestHandler(w http.ResponseWriter, r *http.Request) {
        rqId := uuid.NewRandom()*        rqCtx := logging.WithRqId(httpContext, 
rqId)
*        ...


This one-line style of tagging a context in that last block could be 
supported with this sort of call:

    context var logging.Tags := logging.WithRqID(rqID)

In this instance, as logging.WithRqID is evaluated before the logging.Tags 
context variable is assigned, it accesses the prior value.  The function 
returns the new variable instead of an entire context.Context struct. 

*Context variable scope*

The scope of a context variable would be essentially the same as in context: 
it passes down, but not up. Declaring a context variable without 
immediately assigning it will behave as in context: if it's there, you get 
the prior value.  If it's not, you get a zero value (well, context gives 
you a nil, but the idea is the same).

This style makes it easier to identify use of uninitialized context 
variables:


func badFunc(args... interface{}) {
    context var logging.Logger zap.Logger 
    logging.Logger.Info("in badFunc()", zap.Object("args", args))
    ...
}


It's quite easy to see here that the logging.Logger variable might be being 
used uninitialized.  It also looks awkward.

To compare this to the logging pattern I described in my advent calendar 
post;

func betterFunc(ctx context.Context, args... interface{}) {
    logging.WithContext(ctx).Info("in betterFunc()", 
zap.Object("args",args))
    ...
}

This pattern should hide this, enabling APIs like this:

func goodFunc(args... interface{}) {
    logging.Info("in betterFunc()", zap.Object("args",args))
    ...
}


As this is less awkward than the 'uninitialized context variable' use case, 
I would hope that this style would become more natural and popular than the 
anti-pattern of required context variables.

*Context Cancellation*

This system could be used to re-implement context cancellation; using the 
example from the context documentation:

func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }
        select {
            case <-ctx.Done():
                return ctx.Err()
            case out <- v:
        }
    }
}

This could be written as (assuming the convention is that cancellation 
happens via a context variable in the sync package):

func Stream(out chan<- Value) error {
    context var sync.Done
    for {
        v, err := DoSomething()
        if err != nil {
            return err
        }
        select {
            case <-sync.Done:
                return ctx.Err()
            case out <- v:
        }
    }
}

*Prior Art & Approaches in other languages*

As far as I know, only Perl 6 has this concept as an explicit language 
feature, also called "context variables" at some point (IIRC), which it now 
calls "the * twigil <https://docs.perl6.org/language/variables#The_*_Twigil>
".

Comments/thoughts welcome!
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.
For more options, visit https://groups.google.com/d/optout.

Reply via email to