msanford
msanford

Reputation: 12219

Elegantly run the body of a Ticker before the first interval?

New to Go, I've implemented a small Ticker to poll an API at a given interval:

func Poll() <-chan map[uint8]Data {
    json := make(chan map[uint8]Data)

    user, pass, endpoint := credentials.Get()

    ticker := time.NewTicker(90 * time.Second)

    client := &http.Client{}
    req, _ := http.NewRequest("GET", endpoint, nil)
    req.SetBasicAuth(user, pass)

    go func() {
        for range ticker.C {
            resp, _ := client.Do(req)
            bodyText, _ := ioutil.ReadAll(resp.Body)
            json <- extract(string(bodyText))
        }
    }()

    return json
}

Obviously this waits until the first whole interval has elapsed before calling the API for the first poll; that's not desirable.

This naive (but working) solution seems...weird:

go func() {
    resp, _ := client.Do(req)
    bodyText, _ := ioutil.ReadAll(resp.Body)
    json <- extract(string(bodyText))
}()

go func() {
    for range ticker.C {
        resp, _ := client.Do(req)
        bodyText, _ := ioutil.ReadAll(resp.Body)
        json <- extract(string(bodyText))
    }
}()

Is there a better or more more idiomatic Go way to accomplish this?

Upvotes: 4

Views: 263

Answers (2)

icza
icza

Reputation: 417402

Since the first value on a Ticker's channel is only sent after the ticker's duration, and receiving from a channel blocks until a value is sent on it, you must not receive from the ticker's channel before doing your work. Or in other words: you should receive from the ticker's channel after you do your work first.

One way of achieving that is putting the receive operation into the for loop's post statement, as you can see in Dave's answer. This works perfectly because the post statement is only executed after the loop body, but might not be that intuitive.

Another solution is using a "bare" for loop with no condition (and no init and post statements), doing your work, and receive from the channel in the end:

for {
    resp, _ := client.Do(req)
    bodyText, _ := ioutil.ReadAll(resp.Body)
    json <- extract(string(bodyText))
    <-ticker.C
}

This may be more intuitive, but it's not simpler. But this has one significant advantage compared to the for range solution and compared to Dave's solution: since we're doing the receiving in the body, we may use a select statement to monitor other channels as well. This is important as often there may be other channels or a context.Context that we should monitor and obey.

For example:

// ctx may be a context.Context passed to us or created by us...

go func() {
    defer ticker.Stop() // Don't forget to stop the ticker

    for {
        resp, _ := client.Do(req)
        bodyText, _ := ioutil.ReadAll(resp.Body)
        json <- extract(string(bodyText))

        select {
        case <-ticker.C:
        case <-ctx.Done():
            return
        }
    }
}()

Upvotes: 0

dave
dave

Reputation: 64657

I've done it like this in the past:

for ; true; <-ticker.C {
    resp, _ := client.Do(req)
    bodyText, _ := ioutil.ReadAll(resp.Body)
    json <- extract(string(bodyText))
}

For example:

t := time.NewTicker(2 * time.Second)
now := time.Now()
for ; true; <-t.C {
    fmt.Println(time.Since(now))
}

https://play.golang.org/p/1wz99kzZZ92

Upvotes: 7

Related Questions