如何使用time.After()而不是time.Sleep()获得可中断的暂停

I have a program which periodically checks an external mailbox for messages and which has a user view which allows them to view messages and to terminate the program.

Stripped to minimal features it looks like this

package main

import (
    "log"
    "time"
)

func main() {
    log.Println("Hello, playground")

    quit := make(chan bool)
    data := make(chan string)

    go func() {
        for {
            select {
            case <-quit:
                log.Println("Quitting")
                close(data)
                return
            case data <- fetch():
                // Wait until e.g. exactly 0,10,20,30,40 or 50 mins past the hour
                interval := time.Second * 5
                now := time.Now()
                time.Sleep(now.Truncate(interval).Add(interval).Sub(now))
            }
        }
    }()

    go func() {
        time.Sleep(12 * time.Second) // actually user presses a "quit" button
        quit <- true
    }()

loop:
    for {
        select {
        case info, ok := <-data:
            if !ok {
                break loop
            }
            log.Println("Fetched", info)
        }
    }

    log.Println("Goodbye, playground")
}

func fetch() string {
    log.Println("Fetching")
    return "message"
}

You can run this in the Go Playground

Output is

2009/11/10 23:00:00 Hello, playground
2009/11/10 23:00:00 Fetching
2009/11/10 23:00:00 Fetched message
2009/11/10 23:00:05 Fetching
2009/11/10 23:00:05 Fetched message
2009/11/10 23:00:10 Fetching
2009/11/10 23:00:10 Fetched message
2009/11/10 23:00:15 Fetching
2009/11/10 23:00:15 Quitting
2009/11/10 23:00:15 Goodbye, playground

Notice that

  • "23:00:15 Fetching" is something I didn't expect.
  • The program quits at 23:00:15, not at 23:00:12 because it's sleeping.

The latter would be a problem in my program because it uses a 10 minute sleep between checking for messages. Delaying a quit for that long would make the program seem pretty unresponsive.

From this answer I have learned that you can use time.After() to create a loop delay that can be interrupted.

How should I best apply that to my program?

"23:00:15 Fetching" is something I didn't expect.

This is not surprising, this is the intended working. Quoting from Spec: Select statements:

Execution of a "select" statement proceeds in several steps:

  1. For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the "select" statement.

[...]

So the select statement evaluates the communication operations before it decides on which branch to proceed / execute.

This means that

case data <- fetch():

fetch() will be called, even if sending on data would not be possible and even if receiving from quit can proceed immediately.

Since you have the sleep in one of the case branches, it doesn't matter that quit becomes ready to receive from, checking that (and optionally deciding to go on that branch) has to wait until time.Sleep() –and the whole case branch– completes.

So the communication operation should be a receive operation from the channel returned by time.After(), and only call fetch() in the body of this case branch.

You can make it work like this:

for {
    // Wait until e.g. exactly 0,10,20,30,40 or 50 mins past the hour
    interval := time.Second * 5
    now := time.Now()
    delay := now.Truncate(interval).Add(interval).Sub(now)

    select {
    case <-quit:
        log.Println("Quitting")
        close(data)
        return
    case <-time.After(delay):
        data <- fetch()
    }
}

And now the output (try it on the Go Playground):

2009/11/10 23:00:00 Hello, playground
2009/11/10 23:00:05 Fetching
2009/11/10 23:00:05 Fetched message
2009/11/10 23:00:10 Fetching
2009/11/10 23:00:10 Fetched message
2009/11/10 23:00:12 Quitting
2009/11/10 23:00:12 Goodbye, playground