- [Instructor] The Context package and Go
is a very important package because it's really where we're
gonna implement cancellation and deadlines.
Remember your server can't take forever to do things.
A request can't take forever,
a task can't take forever.
Your database calls, your network calls.
If you allow things to take forever,
you are eventually going to crash when life is bad.
Timeouts, timeouts, timeouts, timeouts,
timeouts, for everything.
And the Context package is the solution.
But Go made it very simple to create these paths
of execution, these goroutines,
it didn't really make it as simple to cancel them,
so we've gotta learn how to use the Context package.
And I'm gonna show you a little bit that,
any API that you have that does any sort of AI,
any sort of Io or eventually will do Io,
should be taking Context as its first parameter.
Now one of the other things that the Context package has
is a generic value bag.
Now I'm very afraid of the generic value bag
because you don't wanna be using it for like requests,
local, we used to call local thread storage right?
You don't wanna be doing things like local request storage.
You never wanna hide data in a context,
but putting data in a context can help for things
that functions don't need,
we'll talk a little bit more about that.
So first I wanna show you how to use the value bag,
but we're gonna be very careful about when we use this.
In code reviews if I see this being used,
we're gonna be very clear,
but we're storing things in there
that aren't really necessary for the functioning of the app,
it's more of for secondary stuff.
Like reporting metrics, things like that.
So the way the value bag works is,
data is stored inside the context with a combination
of a key, and then the value.
But the key isn't just some value,
it's also gonna be based on type.
So the key is a type-based system where then we,
based on some value,
and then we store the value itself on that key.
I didn't explain that very well.
I'm gonna show it to you here, let me show you.
So here's the idea.
I've got a trace ID here.
I've defined this type called TraceID based on a string.
This is the data that we wanna store.
Imagine we have a web service and we're generating
a TraceID, a unique ID.
That is something you probably wanna put in the Context.
And pass that around.
So we've got a TraceID as a string and then I've got a key.
Now the key is based on an integer,
but the type we're gonna be using for a key
is this thing called TraceIDKey.
Based on an int, we're gonna be using this new name type.
Now I go ahead in this program and I create my TraceID.
There it is, there's my TraceID, it's my string.
I can imagine here that here is that string value
for my TraceID and we're calling this right here,
TraceID, there it is.
And I've got this value in here,
which is really representing a UUID right?
There it is.
Now I generate this key,
but understand that the key, the key that I have here,
which is a constant, which is the value of zero,
isn't really an int.
It's really this Trace, let's spell this right.
It's really not an integer,
it's TraceIDKey, remember that.
This value isn't an integer it's a TraceIDKey.
Okay brilliant.
Now what happens?
If we're gonna work with Context we need a context
and when you don't have a context you gotta start
with a parent context.
Now there's two ways to get to a parent context.
We can use this function called Background
out of the Context package,
or you can use what's called TODO
out of the Context package.
Right here, TODO,
that's also another function.
When would you use one over the other?
When you know you need a context but you're not sure
where it's gonna come from,
or how it's gonna be initially initialized, use TODO.
It's a marker in your code to say,
I've gotta come back here and TODO this, right?
And a lot of times you don't really know what your
timeout value should be at.
You don't really know how things are gonna work with Context
but you know you need it,
you wanna lay it in early, very early.
So TODO is something I probably use more than Background.
But Background is your base parent context.
You can think of Background from our perspective,
as a value that's just kind of initialized and setup
as an empty context.
There's nothing in it.
So this is our Background.
Our Background context right here that we're using.
Now Context uses value semantics okay?
We pass a context in, we make changes to it,
we get a new context out.
We use value semantics.
One of the reason we use value semantics with Context is,
is because a context can change throughout
a function call change.
In other words,
at the root of any particular set of function calls,
you might start with that empty Background context.
Then we're gonna pass over these program boundaries.
Maybe we make another function call, and then this function,
even though it starts with the empty,
maybe it adds some timeout information like 10 milliseconds.
Maybe we pass this context around
to two different goroutines.
Now we went from this one goroutine,
to two goroutines right?
We split this path of execution in parallel,
and now things might be changing here.
Now this side of the processing only wants to start using
maybe five millisecond timeouts.
So the value semantics are important because,
as a context is changing, we get a new context,
and then the way cancellation eventually starts to work,
is if we cancel here,
then we're gonna cancel down both paths.
Or maybe we just cancel here,
and we're only gonna cancel down this path.
Value semantics are critical here and making sure
that things are consistent as we make these function calls
and we move down these paths of execution,
and even if they split.
This program is starting with the empty context,
our parent context,
we passed it into a function called WithValue.
WithValue's how you add state to a context.
You give it a key and you give it a value.
Remember the key is based on a type.
So remember what we did here.
We started with the Background or the empty.
The empty context and then what did we get back?
We got a new context, this is our new context now,
we don't care about the old one.
And now the new context has everything the empty one has,
which is nothing, and now what it has is our TraceID,
and then here we're using value semantics again.
So we've got a copy of that UUID and it's based on this key,
which is our TraceIDKey of zero.
That's it, that's what we've stored in the context now.
To pull this data out of the context we're gonna use
the value method off of this context.
The value method.
And the value method actually requires the key,
but not the key value.
It's going to want the key value based on the right type.
So on line 33 I'm passing the key that we used okay,
based on our type, TraceID is the type.
The value is zero.
And when I do that I will get back a copy, UUID,
I will get back a copy.
Again we're all using value semantics here.
A copy of this, we'll get back our copy.
But if you look on line 39, if I just use the value of zero,
which in this case is the constant of kind int.
So if I use an integer,
if I use an integer-based zero, this is an int,
not a TraceID-based value of zero,
but an integer-based value of zero for my key,
then that okay flag's going to be false.
But we're using a blank identifier in this case
'cause I don't care.
It's gonna be false meaning that we didn't get this back.
If I did have a variable where the blank identifier
is on line 39,
then that would be set to a zero value.
So the only way to pull data out of this context
is to not just have the right value,
but have the right value and its right type.
That's what this code here is showing you.
And we've gotta be very, very careful when we store
data int to this context.
This is generic.
We can't be hiding information in our software
ever, ever, ever, so we gotta be very clear about
what we're putting in there
and then how we're taking it out.
But this is how the value bag works with the
WithValue function call and as you'll continue to see,
everything here is value semantics with Context.
Now, let me show you another API here.
In this one what I'm gonna show you
is the wait WithTimeout.
There's actually four With functions here
that you could use.
WithCancel, WithDeadline, WithTimeout, WithValue,
which we saw for the state.
But when it comes to cancellation and deadline,
we have Cancel, Deadline and Timeout.
And they all kinda work the same so,
I'm just gonna focus on Timeout and you'll know already
how Deadline and Cancel work,
and we'll explain that as well.
So here's the idea.
I'm gonna have some goroutine go off and do some work.
And we're gonna be using that cancellation pattern.
The idea now is that goroutine only has so much time
to get the work done.
So let's walk through this code.
I set a duration, here it is.
A duration of 150 milliseconds.
That's what we're willing to wait okay?
And then I go with Context WithTimeout.
Now we're gonna use the Context Background
as our empty base context.
I pass the duration of 150 milliseconds,
and we get back our new context.
So let's think of this context now, as saying,
that we have 150 millisecond Timeout to get things done.
In other words in 150 milliseconds,
this context would report the time is over.
We've gotta stop doing what we're doing.
Now it's very important that you look on line 26.
Notice that WithTimeout,
and this would be the same for the other two functions,
WithDeadline and WithCancel,
you get back a handler function called,
we call it cancel.
You have to call cancel.
It must be called to clean up resources or you will leak.
And so this pattern of calling WithTimeout, WithDeadline,
WithCancel and then deferring the cancel, is critical.
The goroutine that creates the context with one of these
WithTimeout, WithDeadline functions, WithCancel,
must also call cancel and it's okay for cancel
to be called twice.
Anytime you called cancel,
you're immediately timing this thing out,
like immediately we're timing it out.
We're not gonna wait the full time.
So we're deferring the cancel.
It's very, very important.
Notice what we do next.
I create a buffer channel of one,
because we want that delayed guarantee,
and I go off and I create a goroutine.
So here we are again.
Here's our Manager goroutine,
going off created the Employee goroutine.
This Employee again is using that wait for request pattern,
this is a fan-out pattern in a sense,
where that Employee knows that there's some work
it has to do.
This is like a fan-out pattern except
we're only fanning out one goroutine.
Again, we can't,
this goroutine can't cancel the work on its own,
it's gonna require us.
We're saying that this work is gonna be able to take
50 milliseconds in this particular piece of code,
which is well under the 150 that's here.
And then once this is done, it will perform its send,
on line 38, with the result,
which is that data value, result right there.
Okay brilliant.
Now while this goroutine is off doing its work,
in this case the 50 milliseconds of work,
notice that this goroutine here is in the select.
Remember the select lets us manage
multiple channel operations, sends and receives,
at the same time.
And if you look in the first case,
we're now sitting in a receive, right?
We're in a receive right here,
for the data that we're trying to get back
from this goroutine.
We're in the channel receive.
This is that send on the channel,
this is the receive on the channel.
Ideally this is what we want.
We want this send right here, and this receive,
to come together.
We want the send and the receive to come together.
That's our send, the work is done.
This is our receive.
We want that to happen but,
at the same time we're waiting for that,
line 46 is very important.
Notice that we're also on a receive,
we're on a receive also for,
the done function on the context.
So what we're looking at here is this function,
this context, has a method called done.
And we're also going to report here on done.
We're gonna say okay,
if the done function returns before the channel receive
on line 43, hey guess what?
We've timed out.
So basically the Context package is doing this.
It's now sitting,
we get into the select, we're now sitting in this timer,
we're on this call to done.
Once we call done, we're now saying,
we're gonna wait 150 milliseconds,
and if in 150 milliseconds you're not out of this done call,
then we're going to move on.
So at the time we call done, that clock is going to start,
and we're saying we're gonna wait here for 150 milliseconds
and if it doesn't,
then we're gonna send on that receive right there,
we're gonna do the send.
We're going to now be able to move on.
We are hoping however, that this work gets done faster.
Now we've already guaranteed that in this code here,
so if we come over and run this program,
what you're gonna see,
change directory, go build.
What you're gonna see is that the work got completed.
So what happened?
Well we sat in the select case,
either the work gets completed, or we timeout.
Since this was only 50 milliseconds,
we were able to perform this channel send,
with this receive, work completed.
But what if we turn this into five seconds?
What if it's five seconds?
Now we run it.
Look what it says, work canceled.
So we now went to five seconds, what happened?
This goroutine is there, this now,
the done call started a 150 millisecond wait.
150 milliseconds passed, we now received our miss channel,
the done returns a channel,
that's why you see the receive is directly on the done call.
You can see it returns a receive-only channel,
and now we report that this is done and we move on.
Eventually this work finishes, the send has to happen,
or we leak a goroutine and we're back to the ID
of that buffer channel of one.
So this is our cancellation pattern okay?
This is how we do deadlines and timeouts.
A deadline is when you don't give it a duration,
but you give it an actual moment in time
that you want things to happen.
We tend, I tend to use duration-based timeouts much more
than I use the deadline-based timeouts.
But it's all gonna be the same thing okay?
We're gonna be waiting for the goroutine to finish its work,
or we're gonna give it, in this case, 150 milliseconds
to finish the work.
If 150 milliseconds hits, boom, cancellation,
we move on.
And if not then we're gonna get the data
and life is super good.
So this is our base use of the Context package
both for storing values and for dealing
with cancellation and timeout.