I would like to ask how we should approach the issue with context propagation in Golang.
My application is an HTTP JSON API server.
I would use the context as a container of informative data (e.g. the request id, some things I unpack from requests or from the process).
One of the dumbest advantages is to vehicle data and tags useful for statistics and logging. E.g. Be able to add at each log line the transaction id in all the packages I own
The problem I'm facing is the following:
func handleActivityY(w http.ResponseWriter, r *http.Request) {
info, err := decodeRequest(r)
...
stepOne, err := stepOne(r.Context(), info)
...
stepTwo, err := stepTwo(r.Context(), stepOne)
...
}
The problem with this design is the fact the context is an immutable entity (each time we add something or we set a new timeout, we have a new context).
The context cannot be propagated except returning the context at each function call (together with the return value, if any and the error).
The only way to make this work would be:
func handleActivityY(w http.ResponseWriter, r *http.Request) {
ctx, info, err := decodeRequest(r)
...
ctx, stepOne, err := stepOne(ctx, info)
...
ctx, stepTwo, err := stepTwo(ctx, stepOne)
...
}
I've already polluted almost any function in my packages with the context.Context
parameter. Returning it in addition to other parameters seems to me overkill.
Is there really no other more elegant way to do so?
I am currently using the gin framework, which has its own context and it is mutable. I don't want to add the dependency to Gin for that.
Is there really no other more elegant way to do so?
stepOne
could return it's own data independent of context and isolated from how the caller may use its information (ie put it in a databag/context and pass it to other functions)
func handleActivityY(w http.ResponseWriter, r *http.Request) {
ctx, info, err := decodeRequest(r)
...
stepOne, err := stepOne(ctx, info)
...
ctx = ctx.WithValue(ctx, "someContextStepTwoNeeds", stepOne.Something())
stepTwo, err := stepTwo(ctx, stepOne)
...
}
IMO data being passed request scoped should be extremely minimal contextual information
Early in your context pipeline, add a mutable pointer to a data struct:
type MyData struct {
// whatever you need
}
var MyDataKey = /* something */
ctx, cancel := context.WithValue(context.Background(), MyDataKey, &MyData{})
Then in your methods that need to modify your data structure, just do so:
data := ctx.Value(MyDataKey)
data.Foo = /* something */
All normal rules about concurrent access safety apply, so you may need to use mutexes or other protection mechanisms if multiple goroutines can read/set your data value simultaneously.