Context and Cancellation of goroutines



Yesterday I went to the event London Go Gathering, where all the talks had a great level, but particulary Peter Bourgon gave me idea to write about the excelent package context.

Context is used to pass request scoped variables, but in this case I’m only going to focus in cancelation signals.

Lets say that I have a program that execute a long running function, in this case work and we run it in a separate go routine.

package main import ( "fmt" "sync" "time" ) var ( wg sync.WaitGroup ) func work() error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) } } return nil } func main() { fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work() wg.Wait() fmt.Println("Finished. I'm going home") }

$ go run work.go Hey, I'm going to do some work Doing some work 0 Doing some work 1 Doing some work 2 Doing some work 3 ... Doing some work 999 Finished. I'm going home

Now imagine that we have to call that work function from a user interaction or a http request, we probably don’t want to wait forever for that goroutine to finish, so a common pattern is to set a timeout, using a buffered channel, like this:

package main import ( "fmt" "log" "time" ) func work() error { for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) } } return nil } func main() { fmt.Println("Hey, I'm going to do some work") ch := make(chan error, 1) go func() { ch <- work() }() select { case err := <-ch: if err != nil { log.Fatal("Something went wrong :(", err) } case <-time.After(4 * time.Second): fmt.Println("Life is to short to wait that long") } fmt.Println("Finished. I'm going home") }

$ go run work.go Hey, I'm going to do some work Doing some work 0 Doing some work 1 Life is to short to wait that long Finished. I'm going home

Now, is a little bit better because, the main execution doesn’t have to wait for work if it’s timing out.

But it has a problem, if my program is still running like for example a web server, even if I don’t wait for the function work to finish, the goroutine it would be running and consuming resources. So I need a way to cancel that goroutine.

For cancelation of the goroutine we can use the context package. We have to change the function to accept an argument of type context.Context , by convention it’s usuallly the first argument.

package main import ( "fmt" "sync" "time" "golang.org/x/net/context" ) var ( wg sync.WaitGroup ) func work(ctx context.Context) error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) // we received the signal of cancelation in this channel case <-ctx.Done(): fmt.Println("Cancel the context ", i) return ctx.Err() } } return nil } func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished. I'm going home") }

$ go run work.go Hey, I'm going to do some work Doing some work 0 Cancel the context 1 Finished. I'm going home

This is pretty good!, apart that the code looks more simple to manage the timeout, now we are making sure that the function work doesn’t waste any resource.

These examples are good to learn the basics, but let’s try to make it more real. Now the work function is going to do an http request to a server and the server is going to be this other program:

package main // Lazy and Very Random Server import ( "fmt" "math/rand" "net/http" "time" ) func main() { http.HandleFunc("/", LazyServer) http.ListenAndServe(":1111", nil) } // sometimes really fast server, sometimes really slow server func LazyServer(w http.ResponseWriter, req *http.Request) { headOrTails := rand.Intn(2) if headOrTails == 0 { time.Sleep(6 * time.Second) fmt.Fprintf(w, "Go! slow %v", headOrTails) fmt.Printf("Go! slow %v", headOrTails) return } fmt.Fprintf(w, "Go! quick %v", headOrTails) fmt.Printf("Go! quick %v", headOrTails) return }

Randomly is going to be very quick or very slow, we can check that with curl

$ curl http://localhost:1111/ Go! quick 1 $ curl http://localhost:1111/ Go! quick 1 $ curl http://localhost:1111/ *some seconds later* Go! slow 0

So we are going to make an http request to this server, in a goroutine, but if the server is slow we are going to Cancel the request and return quickly, so we can manage the cancellation and free the connection.

package main import ( "fmt" "io/ioutil" "net/http" "sync" "time" "golang.org/x/net/context" ) var ( wg sync.WaitGroup ) // main is not changed func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished. I'm going home") } func work(ctx context.Context) error { defer wg.Done() tr := &http.Transport{} client := &http.Client{Transport: tr} // anonymous struct to pack and unpack data in the channel c := make(chan struct { r *http.Response err error }, 1) req, _ := http.NewRequest("GET", "http://localhost:1111", nil) go func() { resp, err := client.Do(req) fmt.Println("Doing http request is a hard job") pack := struct { r *http.Response err error }{resp, err} c <- pack }() select { case <-ctx.Done(): tr.CancelRequest(req) <-c // Wait for client.Do fmt.Println("Cancel the context") return ctx.Err() case ok := <-c: err := ok.err resp := ok.r if err != nil { fmt.Println("Error ", err) return err } defer resp.Body.Close() out, _ := ioutil.ReadAll(resp.Body) fmt.Printf("Server Response: %s

", out) } return nil }

$ go run work.go Hey, I'm going to do some work Doing http request is a hard job Server Response: Go! quick 1 Finished. I'm going home $ go run work.go Hey, I'm going to do some work Doing http request is a hard job Cancel the context Finished. I'm going home

As you can see in the output, we avoid the slow responses from the server.

In the client the tcp connection is canceled so is not going to be busy waiting for a slow response, so we don’t waste resources.

Happy coding gophers!.