Putting "context" into more context
Welcome to my second blog of putting context into context ?? . For introduction into context you can go check out my first blog.
In our previous interaction, we saw what is context, why we have context and what true purpose this package serves.
In this post, I want to ask more questions and share more answers about context. You might be wondering, What is the structure of the context package? What different methods or functions does context package offers to full fill its true purpose? Is object of context type immutable? If Yessssss, what will happen when we try to change state of an initialised context object. All of these questions will be answered in this blog.
Let's start with context package structure and discuss more about shape of this package.
Context package is made up of Context Interface type which have some primary functions. The primary functions in context type are Deadline(), Done(), Err() & Value(key). In addition to these primary functions there are some supporting functions as well which help you to instantiate or derive context objects.
Put more simply, with the context, we get a couple of things.
The first is a channel (If you are new to Go, I would recommend U to read about channels). There is a function called Done(). When we call this function, it gives us a channel to listen to. If this channel is ready to read, it means that we have been told to stop working. When we get this signal we can use another function called Err(), when we call Err() we get a reason why we've been told to stop working, maybe it's because we've timed out or perhaps because there was an explicit cancellation. The Err function is a very nice way of propagating the reason for context cancellation.
In addition to the above functions, we have another function called Deadline(). You can use the Deadline function to check if a context has a deadline set. One of the restrictions (or deadlines) you can put on a context is: I want you to cancel at this point, no matter what happens. And you can call the deadline function, to know whether a deadline has been set or not and if it is set then for which time it is set for.
Finally Value(key) function. The context object supports request-related data that is actually managed by underlying map; where you can store your request related values. The value function is just a search function for that underlying map.
A few important notes about values. Request scoped values in context object are stored as the underlying map[interface{}] interface{} So they are not type checked at compile time. It's virtually unsafe, which means it's your responsibility to know what's in it, and if you extract it, you should know what kind of values you are getting out. There has been a great amount of talk on what kind of values you should store in a context, and consensus is you should really try to stay as focused as you can on request scoped data so things like authentication information, header information, transaction IDs or the things that are truly tied to the life of request can be stored in the context object.
Until now we discussed some primary functions in the context type but before we move to other supporting functions I would like to discuss an interesting behaviour of context type.
Context is a hard concept to understand because of one particular property it has, the context is not one thing. What does I mean by that?
The context is actually a tree. It has root node and child nodes. Root of Context does not contain any information, but if we try to add more information to the root context, e.g. When we add a cancellation property or timeout, or cast a context to carry a value, we have actually created or added more nodes to the parent node (or root in this case).
The individual nodes in this tree are always immutable. You cannot change an existing context. If you try to do this you are actually creating a new context that points to the parent above. Because of this property, it becomes a tree that can be traversed from below but not from above.
For example, if you want to see when the context will expired, then you have to look up from the bottom to see the first context in this tree that has a timeout property. It's very important that the context is immutable because we pass context around to different go-routines and if they weren't immutable then we would have concurrency issues.
领英推荐
Long story short while talking about context we have to think about trees and sub trees structure and traversal in those trees happen from bottom to top.
Another example before wrapping up this point, if I have a context that someone hands me and I want to add a timeout I will create a new context that points to the old one and the timeout is applied downwards to child nodes. It applies to the context I just created and it applies to any context derived from the one I have now. In other words, all new children point to my context with a timeout, but the timeout doesn't apply to anything above me.So think about this way, I have got an incoming HTTP request and I add a timeout to it and then I start calling other outgoing requests to other microservices. The timeout applies to me and to the other calls below me but it doesn't apply to the http request which I received and started processing it because the context above me doesn't have my timeout. The parent context may have its own thing but it doesn't have mine.
So again, we don't modify context we add subtrees to a context ??
Below is a picture which kind of summaries everything that we just talked about. We can clearly see when we are try to modify context, we actually added a new node to the tree.
But what are these methods that create context and add values, context timeout. I haven't discussed these methods, but I mentioned them above when I said that there are some supporting functions to help you instantiate or derive context objects. These are those supporting functions.
Lets dive into those supporting before writing some good quality code
When you are creating a context you really are saying give me a base instance and then you are adding values and constraints on top of that and creating copies down the change of execution for example you can add values, timeouts and specific cancellation functions to the base context and create a copy out of base context instance.
func Background() Context: There are few functions that we use to derive context, first of them is Background(). This is typically what you would use at your main scope or at your top level scope and all it returns a non nil context, no constrains and no timeouts. So you can use this as parent for anything you are gonna add.
func TODO() Context: Another base level function is a TODO() function. This is more like a placeholder. For example, if you're writing code and you know you need a context instance for your function signature, and it's unclear what context to use or it is not yet available use TODO never send nil for context.
func withCancel(parent Context) (ctx Context, cancel CancelFunc): Next is withCancel function. In withCancel function you are going to give parent context as param to get back a copy of parent context with new Done channel. The context's Done channel is closed when the cancel function is called or parent context channel is closed
func withDeadline(parent Context , deadline time) (ctx Context, cancel CancelFunc): withDeadline is just an expansion of withCancel function that you call it with a parent context and with a deadline duration. After calling you get back a copy of the parent context with the deadline adjusted to be no later than the time parameter. The context done channel is closed when deadline duration expires, when the returned cancel function is called or when parent's Done channel is closed (whichever comes first).
func withTimeout(parent Context, timeout time) (Context, CancelFunc): Next is withTimeout this is effectively like a connivence wrapper around withDeadline function so it is same as creating a context WithDeadline(parent, time.Now().Add(timeout)). It returns a context with the deadline set to the current time plus the value of timeout. If you still are bit confused about what is difference between withDeadline and withTimeout click here
func withValue(parent Context, val interface{}) Context: withValue returns copy of the parent context in which the value for the specified key is set or stored.
Alright, I think we have covered everything that was necessary to know about context. ?? But before I wind up discussion on context, I would like to share some coding examples with you. But let's do that, in a separate Blog.
In my final blog on context I will share a program which will utilise every thing that we have discussed so far. So till then, I would say OLAAAAAAAAAAA :)
Engineer
2 年Good luck!