Mat Ryer · David Hernandez · 3 Feb 2020
Mat Ryer · David Hernandez · 3 Feb 2020
Following a recent conversation with Jon Calhoun, we became interested in cancelling potentially long-running copy operaitons if the context was cancelled, like if a browser cancels an HTTP request.
Context (literally the context.Context
type) has taken hold in Go; visible in the client libraries you use, in the database connections we need, and probably through many layers of your own code in between.
My favourite use of context.Context
is to allow for cancellation. Sometimes it’s fairly easy to see the benefit of aborting something. If you’re processing a list of items ready for a response to an HTTP request, and the user clicks away (the request should in theory get cancelled) then why not stop iterating through that list and let the CPU do something else? In some cases it won’t make much difference but it’s worth familiarising yourself with the pattern.
The net/http
package became context-aware in Go 1.5 with the ctx := r.Context()
and r = r.WithContext(context.Context)
methods to respectively get and use the context with http.Requests.
But in the real world we live alongside older (more traditional? simpler?) designs such as those powerful old types in the io
package, namely io.Reader
and io.Writer
.
context.Context
is an object that you pass through your functions, through your layers of code, which can act as a signal to the whole chain that we want to cancel the work.
If you get the context from the http.Request
via the r.Context()
method, and the user clicks away while the request is processing, the context will be cancelled.
You can wire up your main
programs to cancel a context when the user hits Ctrl+C
. (If they hit Ctrl+C
a second time, you should just exit because otherwise your program will be annoying if it gets stuck somewhere).
When a context is cancelled, calling ctx.Err()
will return a special sentinal error context.Canceled
(notice the US English spelling), which indicates that the context has been cancelled. Similarily, if a context times out, context.DeadlineExceeded
is returned.
As well as explicit cancellation, it’s possible to get a context that cancels itself after a specific duration.
ctx, cancel := context.WithTimeout(ctx, 1 * time.Second)
defer cancel()
err := doSomethingQuickly(ctx)
In this example we use context.WithTimeout
to get a new context that will cancel itself after one second. If doSomethingQuiclky
doesn’t finish within a second, the context will be cancelled and the operation aborted.
You can compare the err
to these values to find out what happened.
if err := ctx.Err(); err != nil {
// time to stop... but why...?
switch err {
case context.Canceled:
// context was cancelled
case context.DeadlineExceeded:
// context timed out
}
}
If the context was cancelled, the error will be context.Canceled
, if it timed out, it will be context.DeadlineExceeded
.
ctx.Done
for concurrent situationsFor code with concurrency designs, the ctx.Done()
channel gets closed when the context is cancelled, allowing you to use it in a select block:
select {
case <-ctx.Done():
// context was cancelled
switch ctx.Err() {
case context.Canceled:
// context was cancelled
case context.DeadlineExceeded:
// context timed out
}
return // stop processing
default:
// do the next piece of work
}
During a conversation with Jon Calhoun where we were talking about how io.Reader
doesn’t use context.Context
in its Read
method:
Read(p []byte) (n int, err error)
You might be forgiven for thinking that cancelling readers (for example, cancelling io.Copy
) is a lost cause, and we should just wait until they add context to all the fundamental interfaces of the standard library (spoiler alert: they aren’t going to do that).
But notice that the Read
method returns an error
, and since we can wrap the Read
method with our own, we can write one that is context-aware.
We can write a method called Reader
which wraps another io.Reader
taking in a context.Context
.
package ctxio
import (
"context"
"io"
)
type readerCtx struct {
ctx context.Context
r io.Reader
}
func (r *readerCtx) Read(p []byte) (n int, err error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.r.Read(p)
}
// NewReader gets a context-aware io.Reader.
func NewReader(ctx context.Context, r io.Reader) io.Reader {
return &readerCtx{
ctx: ctx,
r: r,
}
}
When Read
gets called it first checks to see if the context is finished or not, and only falling through to reading if ctx.Err
returns nil
.
Note this code is inside a package called
ctxio
so the API would bectxio.NewReader
, which is optional - you could just create the helper function near to where you need it.
io.Copy
via contextWe might use this reader when we’re proxying a file for download through a web request:
func handleDownload(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := os.Open("/path/to/absolutely/massive/file.yml")
if err != nil {
http.Error(w, err.Error(), http.InternalServerError)
return
}
defer f.Close()
_, err = io.Copy(w, ctxio.NewReader(ctx, f))
if err != nil {
switch err {
case context.Canceled:
// canceled by user
case context.DeadlineExceeded:
// timed out
default:
// some other error
}
}
}
Since we wrap the io.Reader
(in this case an *os.File
) with ctxio.Reader
, we know that the context error (ctx.Err()
) will be checked on each call to Read
(which happens multiple times inside io.Copy
).
If the *http.Request
is cancelled, we can abort the copy and give some resources back.
contextio
packageOlivier Mengué (@omengue) has already put these ideas into a package which is available at github.com/dolmen-go/contextio if you’re into that sort of thing.
A lot of our blog posts come out of the technical work behind a project we're working on called Pace.
We were frustrated by communication and project management tools that interrupt your flow and overly complicated workflows turn simple tasks, hard. So we decided to build Pace.
Pace is a new minimalist project management tool for tech teams. We promote asynchronous communication by default, while allowing for those times when you really need to chat.
We shift the way work is assigned by allowing only self-assignment, creating a more empowered team and protecting the attention and focus of devs.
We're currently live and would love you to try it and share your opinions on what project management tools should and shouldn't do.
What next? Start your 14 day free trial to see if Pace is right for your team
or you can share the URL directly:
https://pace.dev/blog/2020/02/03/context-aware-ioreader-for-golang-by-mat-ryer.html
Thank you, we don't do ads so we rely on you to spread the word.
If you have any questions about our tech stack, working practices, or how project management will work in Pace, tweet us any time. :)
— pace.dev (@pacedotdev) February 25, 2020
Why you shouldn't use func main in Go #Golang #Patterns
Pace July update: 5 new features #launch #features #update #projectmanagement
Batching operations in Go #Golang #Patterns