53

I have a loop that iterates until a job is up and running:

ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()

started := time.Now()
for now := range ticker.C {
    job, err := client.Job(jobID)
    switch err.(type) {
    case DoesNotExistError:
        continue
    case InternalError:
        return err
    }

    if job.State == "running" {
        break
    }

    if now.Sub(started) > time.Minute*2 {
        return fmt.Errorf("timed out waiting for job")
    }
}

Works great in production. The only problem is that it makes my tests slow. They all wait at least 2 seconds before completing. Is there anyway to get time.Tick to tick immediately?

4
  • These "jobs" are actually running on a remote service. Though it is possible to have this program run a server locally and modify the remote service to accept subscriptions, that solution is much too heavy handed imo. Commented Sep 22, 2015 at 7:27
  • @JimB sorry for my ignorance, but what's the race condition you're referring to around job.State? Is the assumption that job is a shared object? Commented Sep 22, 2015 at 7:29
  • Sorry, I was on mobile, and misread the code thinking that job came from outside the for loop. Commented Sep 22, 2015 at 10:55
  • 2
    @Xavi : wrap your code in a function, use variables for the ticker delay and the timeout value, set those variables to smaller values when testing. Commented Sep 22, 2015 at 15:19

9 Answers 9

84

Unfortunately, it seems that Go developers will not add such functionality in any foreseeable future, so we have to cope...

There are two common ways to use tickers:

for loop

Given something like this:

ticker := time.NewTicker(period)
defer ticker.Stop()
for <- ticker.C {
    ...
}

Use:

ticker := time.NewTicker(period)
defer ticker.Stop()
for ; true; <- ticker.C {
    ...
}

for-select loop

Given something like this:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    select {
        case <- ticker.C: 
            f()
        case <- interrupt:
            break loop
    }
}

Use:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    f()

    select {
        case <- ticker.C: 
            continue
        case <- interrupt:
            break loop
    }
}

Why not just use time.Tick()?

If you're using Go 1.23+, you can safely use time.Tick() instead.

Before Go 1.23:

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".

After Go 1.23:

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.

https://golang.org/pkg/time/#Tick

Sign up to request clarification or add additional context in comments.

Does <- time.Tick(period) create a Ticker on each iteration? According to document, the Ticker created by Tick cannot be garbage-collected. This terrifies me.
@youfu You are absolutely right! I have just edited my answer.
Since go1.23 Ticker will be garbage collected without calling Ticker.Stop()
note that time.Tick is not ticking immediately, so rather irrelevant to the question?
32
ticker := time.NewTicker(period)
for ; true; <-ticker.C {
    ...
}

https://github.com/golang/go/issues/17601

Just to add to this. If you don't want to lose the value you get from the chan you can do for t := time.Now(); true; t = <-ticker.C {
This solutions is also more elegant in cases you need to call continue, inside the for block
6

The actual implementation of Ticker internally is pretty complicated. But you can wrap it with a goroutine:

func NewTicker(delay, repeat time.Duration) *time.Ticker {
    ticker := time.NewTicker(repeat)
    oc := ticker.C
    nc := make(chan time.Time, 1)
    go func() {
        nc <- time.Now()
        for tm := range oc {
            nc <- tm
        }
    }()
    ticker.C = nc
    return ticker
}

Couldn't you just send to the ticker channel immediately ?
It's a receive only channel: type Ticker struct { C <-chan Time // The channel on which the ticks are delivered. // contains filtered or unexported fields }
Why delay arg used here?
"delay" is not used. Also i want point out that this timer is not guaranteed to fire instantly, as the go func(){ } part is sheduled, but not executed for sure. But asides from that nitpicking this should be the accepted answer imho
6

If you want to check the job right away, don't use the ticker as the condition in the for loop. For example:

ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

started := time.Now()
for {
    job, err := client.Job(jobID)
    if err == InternalError {
        return err
    }

    if job.State == "running" {
        break
    }

    now := <-ticker.C
    if now.Sub(started) > 2*time.Minute {
        return fmt.Errorf("timed out waiting for job")
    }
}

If you do still need to check for DoesNotExistError, you want to make sure you do it after the ticker so you don't have a busy-wait.

This is the right solution imo -- also present in the stdlib, see github.com/golang/go/blob/master/src/net/http/server.go#L2648
4

How about using Timer instead of Ticker? Timer can be started with zero duration and then reset to the desired duration value:

timer := time.NewTimer(0)
defer timer.Stop()

for {
    select {
        case <-timer.C:
            timer.Reset(interval)
            job()
        case <-ctx.Done():
            break
    }
}

Comments

3

I cooked up something like this

func main() {
    t := time.Now()
    callme := func() {
        // do somethign more
        fmt.Println("callme", time.Since(t))
    }
    ticker := time.NewTicker(10 * time.Second)
    first := make(chan bool, 1)
    first <- true
    for {
        select {
        case <-ticker.C:
            callme()
        case <-first:
            callme()
        }
        t = time.Now()
    }
    close(first)
}

Isn't it just easier and also clean to call callme() just beofre the for loop? What would be the advantage of complicating it?
2

I will expand on @Bora M. Alper's already good suggestions. If you are using for-select loop, it's better to refactor this:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    select {
        case <- ticker.C: 
            f()
        case <- interrupt:
            break loop
    }
}

to this:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for ; true; <- ticker.C {
    select {
        case <- interrupt: 
            break loop
        default:
            f()
    }
}

This way you do not skip the initial check for interruption and you also gain the first instant tick advantage.

I like this one, very clean
The problem with this is that every iteration you block on the ticker, which means the interrupt case is not waited for at the same moment than the ticker. That might be ok for 1s ticker, but try with 1 minute....
1

I think this might be an interesting alternative for the for-select loop, specially if the contents of the case are not a simple function:

Having:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    select {
        case <- ticker.C: 
            f()
        case <- interrupt:
            break loop
    }
}

Use:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()
firstTick := false

// create a wrapper of the ticker that ticks the first time immediately
tickerChan := func() <-chan time.Time {
  if !firstTick {
    firstTick = true
    c := make(chan time.Time, 1)
    c <- time.Now()
    return c
  }

  return ticker.C
}

loop:
for {
    select {
        case <- tickerChan(): 
            f()
        case <- interrupt:
            break loop
    }
}

Comments

1

You can also drain the channel at the end of the loop:

t := time.NewTicker(period)
defer t.Stop()

for {
    ...
    
    <-t.C
}

Comments

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.