This is it! The last of the context package functions that we will cover in this series. Well, almost. I actually have a couple reader feedback and question emails to respond to, which I’ll do next, before startinga a new series.
Speak of a new series: I’m still looking for suggestions! I have a couple ideas, but I’d rather do something you’re itching to learn about, than whatever random idea I come up with. Please, hit «REPLY» and let me know what you’d like to see next!
It’s nice to end on this note, because it’s a bit of an “advanced usage” function.
func WithoutCancel (added in go1.21.0)
func WithoutCancel(parent Context) ContextWithoutCancel returns a derived context that points to the parent context and is not canceled when parent is canceled. The returned context returns no Deadline or Err, and its Done channel is nil. Calling Cause on the returned context returns nil.
There was some debate about whether or not to add this to the standard library. In the end, it obviously made it. And in my opinion, that’s a good thing.
When would you ever use WithoutCancel? A very common example would be an HTTP handler that needs to do any sort of cleanup or other background task, after responding to the client.
To illustrate, let’s imagine an HTTP handler that sends an email:
func sendEmail(w http.ResponseWriter, r *http.Request) {
to := r.FormValue("to")
subject := r.FormValue("subject")
body := r.FormValue("body")
if err := sendEmail(r.Context(), to, subject, body); err != nil {
http.Error(w, "Failed to send email", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Email is being sent")
}
While this handler is quite simple, it makes for a bad user experience. They won’t get a response from the web server until their email is sent (or fails). This can take a long time, depending on the email server and network conditions.
To improve this, we can move the sending of the email to a background task with an goroutine:
func sendEmail(w http.ResponseWriter, r *http.Request) {
to := r.FormValue("to")
subject := r.FormValue("subject")
body := r.FormValue("body")
go func() {
if err := sendEmail(r.Context(), to, subject, body); err != nil {
log.Error("Failed to send email:", err)
}
}()
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Email is being sent")
}
But now we have a new problem: sendEmail takes a context.Context value, so we pass it the context from the HTTP Request (r.Context()). But this context will be canceled as soon as the response is sent to the client. This means that if the email sending takes longer than the HTTP request, it will be canceled, and the email will not be sent.
This is where context.WithoutCancel comes in handy:
func sendEmail(w http.ResponseWriter, r *http.Request) {
to := r.FormValue("to")
subject := r.FormValue("subject")
body := r.FormValue("body")
go func() {
ctx := context.WithoutCancel(r.Context())
if err := sendEmail(ctx, to, subject, body); err != nil {
log.Error("Failed to send email:", err)
}
}()
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Email is being sent")
}
Now, the sendEmail function will not be canceled when the HTTP request is done. The email will be sent in the background, and the user will get a response immediately. sendEmail will still have access to any values stored in the context, though, which makes this more appropriate than simply passing context.Background() to sendEmail.
What’s more, you could add your own timeout. Perhaps it’s reasonable to wait 5 seconds for the email to be sent. You could do that like this:
func sendEmail(w http.ResponseWriter, r *http.Request) {
to := r.FormValue("to")
subject := r.FormValue("subject")
body := r.FormValue("body")
go func() {
ctx := context.WithoutCancel(r.Context())
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
defer cancel() // Ensure the context is canceled to avoid leaks
if err := sendEmail(ctx, to, subject, body); err != nil {
log.Error("Failed to send email:", err)
}
}()
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Email is being sent")
}