211

Is there a way to do repetitive background tasks in Go? I'm thinking of something like Timer.schedule(task, delay, period) in Java. I know I can do this with a goroutine and Time.sleep(), but I'd like something that easily stopped.

Here's what I got, but looks ugly to me. Is there a cleaner/better way?

func oneWay() {
    var f func()
    var t *time.Timer

    f = func () {
        fmt.Println("doing stuff")
        t = time.AfterFunc(time.Duration(5) * time.Second, f)
    }

    t = time.AfterFunc(time.Duration(5) * time.Second, f)

    defer t.Stop()

    //simulate doing stuff
    time.Sleep(time.Minute)
}
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Steve Brisk
  • 2,503
  • 2
  • 16
  • 13
  • 3
    Thanks for using time.Duration(x) in your example. Every example i could find has a hardcoded int and it complains when you use an int (or float) vars. – Mike Graf Jan 19 '15 at 19:44
  • @MikeGraf you can do `t := time.Tick(time.Duration(period) * time.Second)` where period is an `int` – florianrosenberg Nov 04 '15 at 11:56
  • this solution seems pretty good, IMO. esp. if u simply call f() instead of the outer time.AfterFunc. great for cases where u want to do work x seconds after the work is done, vs. on a consistent interval. – Luke W Feb 28 '17 at 20:16

8 Answers8

322

The function time.NewTicker makes a channel that sends a periodic message, and provides a way to stop it. Use it something like this (untested):

ticker := time.NewTicker(5 * time.Second)
quit := make(chan struct{})
go func() {
    for {
       select {
        case <- ticker.C:
            // do stuff
        case <- quit:
            ticker.Stop()
            return
        }
    }
 }()

You can stop the worker by closing the quit channel: close(quit).

gsamaras
  • 71,951
  • 46
  • 188
  • 305
Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • 18
    The answer is incorrect depending on what the OP wants. If the OP wants an periodical execution regardless of how much time the worker uses, you would have to run `do stuff` in a go routine or else the next worker would execute immediately (when needing more than 5 seconds). – nemo May 09 '13 at 16:36
  • 3
    IMO, you should just `close(quit)` when you want to stop the scheduler. – Dustin May 09 '13 at 17:56
  • Is the quit channel required? Can't we just stop the ticker? – Steve Brisk May 09 '13 at 19:59
  • 4
    Stopping the ticker works, but the goroutine will never be garbage collected. – Paul Hankin May 10 '13 at 17:17
  • @Anonymous thanks, I ended up going with your implementation. Re. stopping the ticker, I'm a little surprised that ticker.Stop() doesn't close the ticker.C channel...Any idea if this is intentional? – Steve Brisk May 10 '13 at 20:33
  • 4
    @SteveBrisk See [the doc](http://golang.org/pkg/time/#Ticker.Stop). If the channel is closed a read would just succeed and you may not want that. – nemo May 19 '13 at 00:11
  • If you want ``do stuff`` to run at most once concurrently (while avoiding timer channel backups if it takes longer than the interval), you should execute in a separate goroutine protected with a mutex. – bk0 May 29 '15 at 01:04
  • 11
    @bk0, time channels don't "backup" (the documentation says "It adjusts the intervals or drops ticks to make up for slow receivers"). The given code does exactly what you say (runs at most one task); if the task takes a long time the next invocation will simply be delayed; no mutex required. If instead it is desired that a new task gets started every interval (even if the previous is not finished) then just use `go func() { /*do stuff */ }()`. – Dave C Aug 27 '15 at 16:31
  • As @nemo pointed out closing the channel still allows the go routine to execute the `do stuff` block (eventually a last time) after the channel has been closed. If you want to avoid that you have to push a value through the `quit` channel before you close it. E.g.: `quit := make(chan bool); /* … go routine call … */; quit <- true; close(quit)` – mxg Apr 02 '17 at 18:07
  • 1
    @PaulHankin "but the goroutine will never be garbage collected." why the goroutine would never be collected? Because the for loop would still be running waiting for tick or a quit channel? – Lukas Lukac Sep 04 '18 at 08:34
59

If you do not care about tick shifting (depending on how long did it took previously on each execution) and you do not want to use channels, it's possible to use native range function.

i.e.

package main

import "fmt"
import "time"

func main() {
    go heartBeat()
    time.Sleep(time.Second * 5)
}

func heartBeat() {
    for range time.Tick(time.Second * 1) {
        fmt.Println("Foo")
    }
}

Playground

Alekc
  • 4,682
  • 6
  • 32
  • 35
32

How about something like

package main

import (
    "fmt"
    "time"
)

func schedule(what func(), delay time.Duration) chan bool {
    stop := make(chan bool)

    go func() {
        for {
            what()
            select {
            case <-time.After(delay):
            case <-stop:
                return
            }
        }
    }()

    return stop
}

func main() {
    ping := func() { fmt.Println("#") }

    stop := schedule(ping, 5*time.Millisecond)
    time.Sleep(25 * time.Millisecond)
    stop <- true
    time.Sleep(25 * time.Millisecond)

    fmt.Println("Done")
}

Playground

Volker
  • 40,468
  • 7
  • 81
  • 87
  • 3
    A `time.Ticker` is better than `time.After` where you'd prefer to keep the task on schedule vs. an arbitrary gap between executions. – Dustin May 09 '13 at 17:55
  • 8
    @Dustin And this is better if you want to perform work with a fixed gap between the end and beginning of the tasks. Neither is best - it's two different use cases. – nos Aug 27 '15 at 13:10
  • ``` // After waits for the duration to elapse and then sends the current time // on the returned channel. // It is equivalent to NewTimer(d).C. // The underlying Timer is not recovered by the garbage collector // until the timer fires. If efficiency is a concern, use NewTimer ``` How about this statement: `If efficiency is a concern, use NewTimer` – lee Jul 04 '19 at 08:57
  • I think maybe this might be a bad example; what() is outside the select and if it is blocking, which would be the primary reason to launch it in a seperate thread, the channels in the select are never read from.. correct ? – Seb Apr 01 '21 at 22:44
26

Check out this library: https://github.com/robfig/cron

Example as below:

c := cron.New()
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("@hourly",      func() { fmt.Println("Every hour") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
c.Start()
Browny Lin
  • 2,427
  • 3
  • 28
  • 32
  • 1
    It will show nothing unless we write a Sleep after that, I know it's because the main thread will be closed before goroutines, but I don't know how to solve it, can you add it to your answer? – Arsham Arya May 02 '22 at 12:14
  • the time is based on system time. there is a issue after change system time. https://github.com/robfig/cron/issues/154 – seamaner Sep 24 '22 at 04:16
10

If you want to stop it in any moment ticker

ticker := time.NewTicker(500 * time.Millisecond)
go func() {
    for range ticker.C {
        fmt.Println("Tick")
    }
}()
time.Sleep(1600 * time.Millisecond)
ticker.Stop()

If you do not want to stop it tick:

tick := time.Tick(500 * time.Millisecond)
for range tick {
    fmt.Println("Tick")
}
John Balvin Arias
  • 2,632
  • 3
  • 26
  • 41
4

I use the following code:

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    fmt.Println("\nToday:", now)

    after := now.Add(1 * time.Minute - time.Second)
    fmt.Println("\nAdd 1 Minute:", after)

    for {
        fmt.Println("test")
        time.Sleep(10 * time.Second)

        now = time.Now()

        if now.After(after) {
            break
        }
    }

    fmt.Println("done")
}

It is more simple and works fine to me.

Zibri
  • 9,096
  • 3
  • 52
  • 44
Gustavo Emmel
  • 310
  • 2
  • 7
3

A broader answer to this question might consider the Lego brick approach often used in Occam, and offered to the Java community via JCSP. There is a very good presentation by Peter Welch on this idea.

This plug-and-play approach translates directly to Go, because Go uses the same Communicating Sequential Process fundamentals as does Occam.

So, when it comes to designing repetitive tasks, you can build your system as a dataflow network of simple components (as goroutines) that exchange events (i.e. messages or signals) via channels.

This approach is compositional: each group of small components can itself behave as a larger component, ad infinitum. This can be very powerful because complex concurrent systems are made from easy to understand bricks.

Footnote: in Welch's presentation, he uses the Occam syntax for channels, which is ! and ? and these directly correspond to ch<- and <-ch in Go.

Rick-777
  • 9,714
  • 5
  • 34
  • 50
3

You can simply use gocron package like this:

package main

import (
 "fmt"
 "time"
 // go get github.com/go-co-op/gocron
 "github.com/go-co-op/gocron"
)

func main() {
 s := gocron.NewScheduler(time.UTC)
 s.Every(3).Seconds().Do(func() { fmt.Println("Every 3 seconds") })
 // you can start running the scheduler in two different ways:
 // starts the scheduler asynchronously
 s.StartAsync()
 // starts the scheduler and blocks current execution path
 // s.StartBlocking()
}

gocron: https://github.com/go-co-op/gocron

Arsham Arya
  • 1,461
  • 3
  • 17
  • 25