gogo-context

Use context to break out of a loop


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?


Solution

  • 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()
            }
    
        }
    }