2

I want to utilize context in golang to be used for cancellation when timeout reached.

The code:

package main

import "fmt"
import "time"
import "context"

func F(ctx context.Context) error {
  ctx, cancel := context.WithTimeout(ctx,3*time.Second)
  defer cancel()
  for i:=0;i<10;i++ {
    time.Sleep(1 * time.Second)
    fmt.Println("No: ",i)
  }
  select {
    case <-ctx.Done():
      fmt.Println("TIME OUT")
      cancel()
      return ctx.Err()
    default:
      fmt.Println("ALL DONE")
      return nil
  }
}

func main() {
  ctx := context.Background()
  err := F(ctx)
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("Success")
  }
}

Expectation: code above should stop running the loop at counter 2, because the timeout is 3 second and looping run 1 second each. So I expect someting like this:

No:  0
No:  1
No:  2
TIME OUT
context deadline exceeded

Actual: What actually happen is the loop keep running until finish even though the context meet timeout and the select listener catch that on <-ctx.Done(). This code prints this:

No:  0
No:  1
No:  2
No:  3
No:  4
No:  5
No:  6
No:  7
No:  8
No:  9
TIME OUT
context deadline exceeded

How to stop the function execution after timeout meet?

1 Answer 1

9

context.Context can only relay the message that timeout or cancellation happened. It does not have the power to actually stop any goroutines (for details, see cancel a blocking operation in Go). The goroutine itself is responsible for checking the timeout and cancellation, and abort early.

You have a loop which unconditionally iterates 10 times and prints something. And you only check the timeout after the loop.

You have to move the context checking into the loop:

func F(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("TIME OUT")
            return ctx.Err()
        default:
            time.Sleep(1 * time.Second)
            fmt.Println("No: ", i)
        }
    }
    fmt.Println("ALL DONE")
    return nil
}

With this change, output will be (try it on the Go Playground):

No:  0
No:  1
No:  2
No:  3
TIME OUT
context deadline exceeded

Note: whether you see "No: 3" printed may or may not happen, as any iteration takes 1 second, and timeout is 3 seconds = 3 * iteration delay, so whether timeout happens first or the 4th iteration begins first is "racy". If you decrease the timeout to like 2900 ms, "No: 3" will not be printed.

Sign up to request clarification or add additional context in comments.

4 Comments

I know the Sleep was probably illustrative in the OP, but if one is in a polling loop that requires a wait, it's better to add a <-time.After(...) to the select block. That way the cancellation will be instant, instead of (worse case) 1 second late.
Looks like it depends on the compiler/version or the machine where it compiled or running. I tried running it in repl.it using ctx.Done() it prints until No: 2 consistently. When I tried it on golang playground using ctx.Done() it always print No: 3, but if I used time.After it consistently prints until No: 2.
Is the cancel() call inside the case <-ctx.Done(): block necessary? Won't the defer after the context initialization call it anyways?
@Sif Yes, you're right, it's not needed. I left it there because I copied the select code form the question, but removed it to avoid confusion.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.