Use context to break out of a loop

6.8k views Asked by At

Consider this (https://play.golang.org/p/zvDiwul9QR0):

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Done")
            break
        default:
            for {
                fmt.Println("loop")
                time.Sleep(500 * time.Millisecond)
            }
        }

    }
}

So here the contexts returns a "Done()" channel after 2 seconds. And I want to catch this and cancel my infinite for loop. The code example above does not do this, it never exits the loop.

How can I achieve this?

1

There are 1 answers

4
colm.anseo On BEST ANSWER

Context cancelation is not magic - it's just a signal mechanism. To abort work, you still need to monitor the state of the context from within your worker goroutine:

func myTask(ctx context.Context, poll time.Duration) error {                 
    ticker := time.NewTicker(poll)
    defer ticker.Stop()
                                                                 
    for {                                                                      
        fmt.Println("loop")                                                      
        select {                                                                 
        case <-ticker.C:
        case <-ctx.Done():
            return ctx.Err()
        }                                                                        
    }                                                                          
}                                                                     
                                                                         

https://go.dev/play/p/s6dSr9qqKJU


also as Eli pointed out, break will only break out of the select statement - so you need something more precise to break out of a loop. Refactoring into functions make return's much more intuitive for task abortion.


Following up from comments. I would refactor your task like so:

// any potentially blocking task should take a context
// style: context should be the first passed in parameter
func myTask(ctx context.Context, poll time.Duration) error {
    ticker := time.NewTicker(poll)
    defer ticker.Stop()
    for {
        fmt.Println("loop")
        select {
        case <-ticker.C:
        case <-ctx.Done():
            return ctx.Err()
        }

    }
}