8

I need to read data from the Go channel for a certain period of time (say 5 seconds). The select statement with timeout doesn't work for me, as I need to read as many values as available and stop exactly after 5 seconds. So far, I've come up with a solution using an extra time channel https://play.golang.org/p/yev9CcvzRIL

package main

import "time"
import "fmt"

func main() {
    // I have no control over dataChan
    dataChan := make(chan string)
    // this is a stub to demonstrate some data coming from dataChan
    go func() {
        for {
            dataChan <- "some data"
            time.Sleep(time.Second)
        }
    }()

    // the following is the code I'm asking about
    timeChan := time.NewTimer(time.Second * 5).C
    for {
        select {
        case d := <-dataChan:
            fmt.Println("Got:", d)
        case <-timeChan:
            fmt.Println("Time's up!")
            return
        }
    }
}

I'm wondering is there any better or more idiomatic way for solving this problem?

vitr
  • 6,766
  • 8
  • 30
  • 50
  • please, consider `go func()` as a stub, I don't create the data channel, don't send to it anything and have no power to close it – vitr Apr 17 '18 at 08:18

2 Answers2

14

That's pretty much it. But if you don't need to stop or reset the timer, simply use time.After() instead of time.NewTimer(). time.After() is "equivalent to NewTimer(d).C".

afterCh := time.After(5 * time.Second)
for {
    select {
    case d := <-dataChan:
        fmt.Println("Got:", d)
    case <-afterCh:
        fmt.Println("Time's up!")
        return
    }
}

Alternatively (to your liking), you may declare the after channel in the for statement like this:

for afterCh := time.After(5 * time.Second); ; {
    select {
    case d := <-dataChan:
        fmt.Println("Got:", d)
    case <-afterCh:
        fmt.Println("Time's up!")
        return
    }
}

Also I know this is just an example, but always think how a goroutine you start will properly end, as the goroutine producing data in your case will never terminate.

Also don't forget that if multiple cases may be executed without blocking, one is chosen randomly. So if dataChan is ready to receive from "non-stop", there is no guarantee that the loop will terminate immediately after the timeout. In practice this is usually not a problem (starting with that even the timeout is not a guarantee, see details at Golang Timers with 0 length), but you should not forget about it in "mission-critial" applications. For details, see related questions:

force priority of go select statement

golang: How the select worked when multiple channel involved?

icza
  • 389,944
  • 63
  • 907
  • 827
  • thank you, and, yes, `go func()` is a dumb example, I added some comments in the code – vitr Apr 17 '18 at 08:39
  • 2
    @vitr, if dataChan provides values non-stop, the select will be [pseudo-random](https://golang.org/ref/spec#Select_statements) after 5 seconds and may not hit the afterCh case reliably. If you cannot tolerate that, check afterCh explicitly before attempting to receive from dataChan: https://play.golang.org/p/3_odFeXbWLf – Peter Apr 17 '18 at 08:41
  • @Peter Good tip, that question has been surfaced a couple of times. Added links to those questions. – icza Apr 17 '18 at 08:46
  • 1
    @Peter And in your example, you actually check the `dataChan` twice, I think you meant to check the `afterCh` twice. – icza Apr 17 '18 at 08:54
  • thank you @Peter, so, in the edge case when the timer and the reciever occur in exact moment in time, the timer may fail (randomly). – vitr Apr 17 '18 at 09:02
  • @icza you mentioned "immediately" after timeout in the edited answer, does it mean the timeout may happen a moment later or is there a chance to miss it forever? – vitr Apr 17 '18 at 09:05
  • 1
    @vitr **If** `dataCh` is "always" ready, then after the timeout the chance to miss it forever is zero. To get a rough picture: chance to miss it once is 0.5. Chance to miss it twice is 0.25. Chance to miss it three times is 0.125. Chance to miss it after 10 iterations is less than 0.001. Chance to miss it after `n` iterations is `2 ^ -n`. Also there is no guarantee that a timeout will happen immediately after 5 seconds. Please read the linked answer for details: [Golang Timers with 0 length](https://stackoverflow.com/questions/43616295/golang-timers-with-0-length/43616551#43616551). – icza Apr 17 '18 at 09:07
  • @icza the iteration duration in this example is 5 seconds, that means I'll be reading for 10 second, then 15, etc. this is actually scary) I'd prefer an extra check then – vitr Apr 17 '18 at 09:12
  • 1
    @vitr No, you misunderstood. We're talking about the `for` loop iterations, **not** the iterations of the timer. The timer will only fire once. So for example if `dataChan` is always ready and processing a data value takes let's say 0.5 ms, then 10 additional iterations would only take 5 seconds + 5 ms, and it would terminate with 99.9% probability. And this only means if `dataChan` is _truly_ always ready to receive from. If just in one iteration it would not be ready, the timeout channel would be chosen with 100% probability. – icza Apr 17 '18 at 09:13
  • I see, kk, that sounds better – vitr Apr 17 '18 at 09:15
3

It looks like context with deadline would be good fit, something like

func main() {
    dataChan := make(chan string)

    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    defer cancel()

    go func(ctx context.Context) {
        for {
            select {
            case dataChan <- "some data":
                time.Sleep(time.Second)
            case <-ctx.Done():
                fmt.Println(ctx.Err())
                close(dataChan)
                return
            }
        }
    }(ctx)

    for d := range dataChan {
        fmt.Println("Got:", d)
    }

}
ain
  • 22,394
  • 3
  • 54
  • 74
  • thank you, it's an interesting approach in general, but doesn't fit my particular case, as I don't control the dataChan – vitr Apr 17 '18 at 08:20
  • You don't have to close the channel in the "generator routine", I did it just so that the reader loop is simpler. Just send the channel in as a parameter (like context is right now) and remove the `close(dataChan)`. Of course if you do it with the simple examle given here you'll have infinite loop (when goroutine ends, no more items is sent to the channel but range loop will not end) but in your real code you presumaly have more complex structure anyway... – ain Apr 17 '18 at 08:46
  • gotcha, what about that randomness issue from icza's answer? does the context suffer from the same? – vitr Apr 17 '18 at 09:17
  • 2
    @vitr This randomness is not a "property" of `Context`, it's in the "nature" of the `select` statement. So wherever you see a `select` statement, it is in "play". – icza Apr 17 '18 at 09:20