Hi guys, I am back again with an another short tutorial. This time we will be looking at an interesting use case for Golang ticker. Apart from tickers we will also make use of channels in this tutorial. So, without any further ado, let’s get started
Agenda
Recently, while working on a small project of mine, I came across a need to use time.NewTicker() that ticks only at specific seconds of every minute, such as at 3rd, 23rd, and 37th second every minute in my case. I knew that time.NewTicker() can only generate tickers that tick every second or minute because it accepts a duration as a parameter. Therefore, I could not use it directly in my case. In this article, I will be walking through how I got it working, which includes the use of channels and go-routines. Hint, it includes use of channels and go-routines.
Note: There are of course other out-of-the-box solutions like cron job that can be used to achieve the same outcome but in this tutorial our focus will be on using channels and tickers instead.
Approach 1 – channel feed by multiple golang ticker
Well, the very first thing that may come to our minds is to create multiple tickers and use a channel to pass on the ticks, like the example below:
func NewTickEveryMinute() (chan<- struct{}, func()) {
// we create 3 separate tickers
ticker_3_Second := time.NewTicker(1 * time.Minute)
ticker_23_Second := time.NewTicker(1 * time.Minute)
ticker_37_Second := time.NewTicker(1 * time.Minute)
ch := make(chan struct{}, 0)
exitSignal := make(chan struct{}, 0)
// we call stop from parent to stop all the 3 tickers
stop := func() {
// closing exitSignal will return from below go routine
close(exitSignal)
ticker_3_Second.Stop()
ticker_23_Second.Stop()
ticker_37_Second.Stop()
}
go func() {
for {
select {
case <-exitSignal:
return
case <-ticker_3_Second.C:
ch <- struct{}{}
case <-ticker_23_Second.C:
ch <- struct{}{}
case <-ticker_37_Second.C:
ch <- struct{}{}
}
}
}()
return ch, stop
}
func Test(ctx context.Context) {
ticker, stop := NewTickEveryMinute()
// we call the stop when exiting from this function
defer stop()
for {
select {
case <-ctx.Done():
return
case <-ticker:
fmt.Printf("Tick at %d second", time.Now().Second())
}
}
}
Now, in our Test function, we are trying to use it as shown above. We have also added ctx.Done() which returns a channel and provides us with an opportunity to exit the infinite for loop we have in our Test function.
Now, if you execute the Test function, the output is not what we initially expected it to be.
// expected output
Tick at 3 second
Tick at 23 second
Tick at 37 second
// actual output will be something like
Tick at 8 second
Tick at 8 second
Tick at 8 second
So, why are we receiving a tick from all of our 3 tickers at exactly the same second? Also, we aren’t even ticking at any one of our initially required 3rd, 23rd, or 37th second, but instead ticking at any random seconds like 8 in the above case.
To understand this behavior, you must first understand that when we create a ticker, for example, using time.NewTicker(1 * time.Minute), there is no way for it to know at which specific second of the minute it should tick. So, what it does is take the exact time as the initial starting point when it is invoked. In the above case, we happened to run the code at approximately the 8th second past the minute, and therefore it prints Tick at 8 seconds.
Now that we have answered the question ‘why ‘Tick at 8 second‘?, it may be obvious to you by now that since all the 3 tickers started at almost the exact time when we ran the code, therefore they all tick at the same time, which doesn’t meet our requirement.
Approach 2 – Create a golang ticker that ticks at specified second
Our last example did not work as expected. So, what can we do now? Well, since we now know that ‘time.NewTicker‘ creates a ticker with a reference to the exact time it was invoked, we can utilize this information to create a ticker that ticks at any specified second. Let’s explore an example of how we can achieve this.
func NewTickEveryMinute(second int) (chan<- struct{}, func()) {
now := time.Now()
sleepDur := time.Duration((60-now.Second()+second)%60) * time.Second
// we sleep here for sleepDur before creating our ticker
time.Sleep(sleepDur)
// we create a tickers
ticker_3_Second := time.NewTicker(1 * time.Minute)
ch := make(chan<- struct{}, 0)
exitSignal := make(chan struct{}, 0)
// we call stop from parent to stop our ticker
stop := func() {
// closing exitSignal will return from below go routine
close(exitSignal)
ticker_3_Second.Stop()
}
go func() {
for {
select {
case <-exitSignal:
return
case <-ticker_3_Second.C:
ch <- struct{}{}
}
}
}()
return ch, stop
}
We modified our ‘NewTickEveryMinute‘ function to accept a target second. Using this, we added a sleep function until the specified second arrives. Then, we created a ticker using ‘time.NewTicker(1 * time.Minute)‘. This ensures that our ticker’s reference time is almost equal to our specified second. Now, let’s test our updated code.
func Test(ctx context.Context) {
// we have set our target second to 3 seconds
ticker, stop := NewTickEveryMinute(3)
// we call the stop when exiting from this function
defer stop()
for {
select {
case <-ctx.Done():
return
case <-ticker:
fmt.Printf("Tick at %d second", time.Now().Second())
}
}
}
Our output will now look something like the following, which is very close to what we wanted. We are making progress!
// expected output
Tick at 3 second
Tick at 23 second
Tick at 37 second
// new output will be something like
1st Minute -> Tick at 3 second
2nd Minute -> Tick at 3 second
3rd Minute -> Tick at 3 second
Approach 3 – Create the final golang ticker using channel and go-routines.
Now that our ‘Approach 2‘ function is working as expected, we can extend it to meet our final requirement. Let’s see how we can achieve this.
Before we begin creating our new ‘TickEveryMinute‘ function, let’s define a new type called ‘CustomTicker‘, as shown below.
type CustomTicker struct {
Ch chan struct{}
Tickers []*time.Ticker
exitSignal chan struct{}
}
As you can see, we have moved both of the channels we used in the previous examples inside a struct. Additionally, we have added a slice of Tickers which we will touch upon shortly.
Let’s also implement a subset of the time.Ticker’s interface, as shown below.”
// below will stop all tickers in tickers slice
func (c *CustomTicker) Stop() {
c.ExitSignal <- struct{}{}
for _, t := range c.Tickers {
t.Stop()
}
}
// we add a ticker to tickers slice
func (c *CustomTicker) Add(ticker *time.Ticker) {
c.Tickers = append(c.Tickers, ticker)
}
Now let’s create our NewTickEveryMinute function once again
func NewTickEveryMinute(seconds []uint8) *CustomTicker {
customTicker := CustomTicker{
Ch: make(chan struct{}),
}
for _, second := range seconds {
if second > 60 {
continue
}
// for each second we create a new ticker in separate goroutines
// and all the ticker pass on their ticks to our customTicker.Ch
go func(second_ uint8, t *CustomTicker) {
now := time.Now()
sleepDur := time.Duration((60-now.Second()+int(second_))%60) * time.Second
time.Sleep(sleepDur)
ticker := time.NewTicker(time.Minute)
t.Add(ticker)
for {
select {
case <-t.ExitSignal:
return
case <-ticker.C:
t.C <- struct{}{}
}
}
}(second, &customTicker)
}
return &customTicker
}
Now in our new NewTickEveryMinute, a few major changes to observe.
- We are accepting a slice of seconds instead of a single second.
- Our return values have changed and now we are only returning a pointer to our customTicker object.
- For each second in our seconds slice we are creating a new ticker in a separate go-routine. Inside of our go-routine we have the same sleep code as in your approach 2.
Let’s test our new NewTickEveryMinute function and see how it performs.
func Test(ctx context.Context) {
// we pass is our target seconds slice
ticker := NewTickEveryMinute([]int{3, 23, 37})
// here we call our CustomTicker's pointer receiver function stop
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker:
fmt.Printf("Tick at %d second", time.Now().Second())
}
}
}
// expected output
Tick at 3 second
Tick at 23 second
Tick at 37 second
// Our new output is the same as our initial requirement. Congrates
1st Minute -> Tick at 3 second
2nd Minute -> Tick at 23 second
3rd Minute -> Tick at 37 second
Conclusion
In this article, we explored how to use Golang’s time package to create tickers that tick at specific seconds of every minute. We learned about the limitations of the time.NewTicker function and how to work around them by using channels and go-routines. By creating a CustomTicker type and implementing a subset of the time.Ticker interface, we were able to achieve our goal of creating tickers that tick at any specified second. With these techniques, we can create precise tickers that meet the needs of our applications