Reputation: 12219
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
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
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