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 [email protected].
For more options, visit https://groups.google.com/d/optout.