Used a second channel as Volker suggested. This is what I ended up running with:
package main
import (
"log"
"time"
)
// Run the function every tick
// Return false from the func to stop the ticker
func Every(duration time.Duration, work func(time.Time) bool) chan bool {
ticker := time.NewTicker(duration)
stop := make(chan bool, 1)
go func() {
defer log.Println("ticker stopped")
for {
select {
case time := <-ticker.C:
if !work(time) {
stop <- true
}
case <-stop:
return
}
}
}()
return stop
}
func main() {
stop := Every(1*time.Second, func(time.Time) bool {
log.Println("tick")
return true
})
time.Sleep(3 * time.Second)
log.Println("stopping ticker")
stop <- true
time.Sleep(3 * time.Second)
}
Answer from whatupdave on Stack Overflowgo - Ticker Stop behaviour in Golang - Stack Overflow
Tickers on gobyexample
go - Golang time.Ticker triggers twice after blocking a while - Stack Overflow
go - Correct way to test code that uses time.Ticker? - Stack Overflow
Videos
Somehow I missed this in the release notes, but I was soo happy to read this today. All the little things add up to make life better!
Before Go 1.23, this documentation warned that the underlying Ticker would never be recovered by the garbage collector, and that if efficiency was a concern, code should use NewTicker instead and call Ticker.Stop when the ticker is no longer needed. As of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do.
Used a second channel as Volker suggested. This is what I ended up running with:
package main
import (
"log"
"time"
)
// Run the function every tick
// Return false from the func to stop the ticker
func Every(duration time.Duration, work func(time.Time) bool) chan bool {
ticker := time.NewTicker(duration)
stop := make(chan bool, 1)
go func() {
defer log.Println("ticker stopped")
for {
select {
case time := <-ticker.C:
if !work(time) {
stop <- true
}
case <-stop:
return
}
}
}()
return stop
}
func main() {
stop := Every(1*time.Second, func(time.Time) bool {
log.Println("tick")
return true
})
time.Sleep(3 * time.Second)
log.Println("stopping ticker")
stop <- true
time.Sleep(3 * time.Second)
}
Signal "done" on a second channel and select in your goroutine between ticker and done channel.
Depending on what you really want to do a better solution might exist, but this is hard to tell from the reduced demo code.
Can someone please explain what's the need for the `done` channel in the ticker example on gobyexample?
https://gobyexample.com/tickers
Is this a well known pattern?
The only reason that comes to my mind is that it makes it possible to stop that ticker goroutine at any time. Otherwise, without the done channel the example still seems complete and works as intended.
The ticker creates a buffered channel with a capacity of one. The ticker drops time values when the channel is full.
Here's the timeline for the trace in the question. Events listed for the same time happen a tiny bit after each other in the order listed.
1s - ticker sends value
1s - main goroutine receives first value
1s - main goroutine starts sleep for 5s
2s - ticker sends value (channel has capacity 1).
3s - ticker fails to send value because channel is full
4s - ticker fails to send value because channel is full
5s - ticker fails to send value because channel is full
6s - main goroutine exits sleep and receives buffered value
6s - ticker sends value
6s - main goroutine receives buffered value
The ticker and main goroutine race to the 6s mark because the sleep is a multiple of the ticker period. In your trace, the main goroutine wins the race. On my machine, the ticker wins the race and I get the timing that you expect. Here's what it looks like on my machine.
...
5s - ticker fails to send value because channel is full
6s - ticker fails to send value because channel is full
6s - main goroutine receives buffered value
7s - ticker sends value
7s - main goroutine receives value
The ticker wins the race to the 6s mark on the playground. Run it here. The main goroutine wins the race when the sleep time is nudged down by a 1µs. Run it here.
This is easy to see when the sleep duration is not a multiple of the ticker period. Here's what happens when the sleep duration is 2.5 seconds:
1s - ticker sends value
1s - main goroutine receives first value
1s - main goroutine starts sleep for 2.5s
2s - ticker sends value
3s - ticker fails to send value because channel is full
3.5s - main goroutine exits sleep and receives buffered value
4s - ticker sends value
4s - main goroutine receives value
Run the last example on the playground.
In time.Ticker, there is a chan Time. In the NewTicker(d Duration) *Ticker function, It creates tickers Time channel as a buffered channel with capacity 1. In the function, it is mentioned as below.
// Give the channel a 1-element time buffer.
// If the client falls behind while reading, we drop ticks
// on the floor until the client catches up.
In your case, time ticks in every second and write to the channel. While sleeping, the ticker writes one time to the channel, and after the loop continues it is immediately received by select case and print. For the other 4 seconds, the channel is blocking and ticks are dropped. Immediately loop continues it is unblocked and accept more ticks to the channel.
Print the time comes through the channel and you can easily understand the behavior. Updated code as below.
func main() {
ticker := time.NewTicker(1 * time.Second)
i := 0
for {
select {
case t := <-ticker.C:
log.Println(i, t)
if i == 0 {
time.Sleep(5 * time.Second) // simulate the blocking
}
i++
}
}
}
Output:
2022/02/20 08:47:58 0 2022-02-20 08:47:58.773375 +0530 +0530 m=+1.004241004
2022/02/20 08:48:03 1 2022-02-20 08:47:59.772787 +0530 +0530 m=+2.003666993
2022/02/20 08:48:03 2 2022-02-20 08:48:03.774272 +0530 +0530 m=+6.005207433 // printing time is same, but channel's received time has 4 sec difference.
2022/02/20 08:48:04 3 2022-02-20 08:48:04.774203 +0530 +0530 m=+7.005151715
Run in Playground.
Since this is a pretty simple function, I assume you are just using this as an example of how to mock non-trivial stuff. If you actually wanted to test this code, rather than mocking up ticker, why not just use really small intervals.
IMHO the 2nd option is the better of the two, making a user call:
foo(dur, NewTicker(interval)...
doesn't seem like much of a burden.
Also having the callback is serious code smell in Go:
func Countdown(ticker Ticker, duration time.Duration) chan time.Duration {
remainingCh := make(chan time.Duration, 1)
go func(ticker Ticker, dur time.Duration, remainingCh chan time.Duration) {
for remaining := duration; remaining >= 0; remaining -= ticker.Duration() {
remainingCh <- remaining
ticker.Tick()
}
ticker.Stop()
close(remainingCh)
}(ticker, duration, remainingCh)
return remainingCh
}
You could then use this code like:
func main() {
for d := range Countdown(NewTicker(time.Second), time.Minute) {
log.Printf("%v to go", d)
}
}
Here it is on the playground: http://play.golang.org/p/US0psGOvvt
This doesn't answer the how to inject the mock part, but it seems like you are trying too hard. if the example is representative of what you actually are testing, then just use small numbers.
http://play.golang.org/p/b_1kqyIu-u
Countdown(5, 1, func(d time.Duration) {
log.Printf("%v to go", d)
})
Now, if you are testing code that calls Countdown (rather than testing Countdown), then I'd probably just create a flag you can set for your module that scales the numbers to be as fast possible with the same invocation count.
http://play.golang.org/p/KqCGnaR3vc
if testMode {
duration = duration/interval
interval = 1
}
The reason this does not work in the https://play.golang.org/ is because time.Tick(...) is a dirty function, which is only safe to be used in an infinite loop that ends with the app (or other use cases where you don't mind leaking memory). As per Golang documentation:
Tick is a convenience wrapper for NewTicker providing access to the ticking channel only. While Tick is useful for clients that have no need to shut down the Ticker, be aware that without a way to shut it down the underlying Ticker cannot be recovered by the garbage collector; it "leaks". Unlike NewTicker, Tick will return nil if d <= 0.
So generally it's better to use time.NewTicker(...) instead. See example at: https://pkg.go.dev/time#example-NewTicker
UPDATE: After Go v1.23 the Go compiler has removed the above limitation, so now both versions are ok to be used:
Before Go 1.23, this documentation warned that the underlying Ticker would never be recovered by the garbage collector, and that if efficiency was a concern, code should use NewTicker instead and call Ticker.Stop when the ticker is no longer needed. As of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do.
Play.golang.org has some strict rules to protect it. If you run this locally, it works.