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