In a lot of situations, we would like to be able to schedule functions in Go. While there are many current tools for doing scheduling (such as Cron), I would much prefer if the entire working of my program was contained in the same code/binary and I did not have to go to the system crontab to discover what is going on.
Thankfully, the time
standard library already comes with A LOT of tools to schedule events. Let's look at some of them and when to use it.
Check out Kronika, a package that adds some nice helpers around the time package.
Using time.After()
time.After()
allows us to perform an action after a duration. For example:
NOTE: The helper constants time.Second
, time.Minute
and time.Hour
are all of type time.Duration
. So if we have to supply a duration, we can multiply these constants by a number (e.g. time.Second * 5
) and the expression returns a time.Duration
.
package main
import (
"fmt"
"time"
)
func main() {
// This will block for 5 seconds and then return the current time
theTime := <-time.After(time.Second * 5)
fmt.Println(theTime.Format("2006-01-02 15:04:05"))
}
2019-09-22 09:33:05
Using time.Ticker
time.After()
is great for one-time actions, but the power of cron jobs are in performing repeated actions.
So, to do that we use a time.Ticker
. For most use cases, we can use the helper function time.Tick()
to create a ticker. For example:
package main
import (
"fmt"
"time"
)
func main() {
// This will print the time every 5 seconds
for theTime := range time.Tick(time.Second * 5) {
fmt.Println(theTime.Format("2006-01-02 15:04:05"))
}
}
2019-09-22 10:07:54
2019-09-22 10:07:59
2019-09-22 10:08:04
2019-09-22 10:08:09
2019-09-22 10:08:14
NOTE: When using a Ticker
, the first event will be triggered AFTER the delay.
Dangers of using time.Tick()
When we use the time.Tick()
function, we do not have direct access to the underlying time.Ticker
and so we cannot close it properly.
If we never need to explicitly stop the ticker (for example if the ticker will run all the time), then this may not be an issue. However, if we simply ignore the ticker, the resources will not be freed up and it will not be garbage collected.
Limitations using time.Tick()
There are several things we can't easily do with time.Ticker
:
- Specify a start time
- Stop the ticker
Extending time.Tick()
using a custom function
To overcome the limitations of time.Tick()
, I've created a helper function which I use in my projects.
func cron(ctx context.Context, startTime time.Time, delay time.Duration) <-chan time.Time {
// Create the channel which we will return
stream := make(chan time.Time, 1)
// Calculating the first start time in the future
// Need to check if the time is zero (e.g. if time.Time{} was used)
if !startTime.IsZero() {
diff := time.Until(startTime)
if diff < 0 {
total := diff - delay
times := total / delay * -1
startTime = startTime.Add(times * delay)
}
}
// Run this in a goroutine, or our function will block until the first event
go func() {
// Run the first event after it gets to the start time
t := <-time.After(time.Until(startTime))
stream <- t
// Open a new ticker
ticker := time.NewTicker(delay)
// Make sure to stop the ticker when we're done
defer ticker.Stop()
// Listen on both the ticker and the context done channel to know when to stop
for {
select {
case t2 := <-ticker.C:
stream <- t2
case <-ctx.Done():
close(stream)
return
}
}
}()
return stream
}
What's happening in the function
The function receives 3 parameters.
- A
Context
: The ticker will be stopped whenever the context is cancelled. So we can create a context with a cancel function, or a timeout, or a deadline, and when that context is cancelled, the function will gracefully release it's resources. - A
Time
: The start time is used as a reference to know when to start ticking. If the start time is in the future, it will not start ticking until that time. If it is in the past. The function calculates the first event that will be in the future by adding an appropriate multiple of the delay. - A
Duration
: This is the interval between ticks. Calculated from the start time.
Examples of using the custom function
Run on Tuesdays by 2pm
ctx := context.Background()
startTime, err := time.Parse(
"2006-01-02 15:04:05",
"2019-09-17 14:00:00",
) // is a tuesday
if err != nil {
panic(err)
}
delay := time.Hour * 24 * 7 // 1 week
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Run every hour, on the hour
ctx := context.Background()
startTime, err := time.Parse(
"2006-01-02 15:04:05",
"2019-09-17 14:00:00",
) // any time in the past works but it should be on the hour
if err != nil {
panic(err)
}
delay := time.Hour // 1 hour
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Run every 10 minutes, starting in a week
ctx := context.Background()
startTime, err := time.Now().AddDate(0, 0, 7) // see https://golang.org/pkg/time/#Time.AddDate
if err != nil {
panic(err)
}
delay := time.Minute * 10 // 10 minutes
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Conclusion
With this function, I have much better control over scheduling in my projects. Hopefully, it is also of some use to you.
Check out Kronika, a package that adds some nice helpers around the time package.
Let me know what you think.
The post Better scheduling in Go appeared first on Stephen AfamO's Blog.
Top comments (0)