JavaRush /Courses /Go SELF /Timers and tickers — time.After, time.NewTicker

Timers and tickers — time.After, time.NewTicker

Go SELF
Level 34, Lesson 3
Available

1. Why do we need timers and tickers if we have Sleep

When you first see “time-based waiting” in Go, it looks like one tool solves everything: time.Sleep(d). It really is useful, but it is more like “put the program on pause”. In real tasks, we often want not just a pause, but an event: “in 2 seconds, do an action” or “every 5 seconds, run a check”.

That is where timers and tickers show up: they give us a neat waiting mechanism that is easy to embed into loop logic and reads like “wait for a time signal”.

Imagine the difference at an everyday level. Sleep is like “close your eyes and count to ten”. After is like “set an alarm and wait for it to ring”. Ticker is like a “metronome”: tick-tick-tick until you stop it.

Signal channel: what time.After and ticker.C return

Now there will be one word you have not studied systematically yet: channel. Full channel coverage will come later, but today we need a “local minimum” so the lecture does not turn into a 12-season series.

time.After(d) returns a value of type “time channel” (in practice, it is <-chan time.Time). time.NewTicker(d) has a field C, which is also such a channel (also <-chan time.Time). Simplifying: it is something you can use to receive a “time has arrived” signal once (or many times).

The main rule of today’s lecture: we do not create channels ourselves, we do not send values into them, and we do not close them. We do only one action: read the event with the <- operator.

Reading looks like this:

<-time.After(200 * time.Millisecond)

This reads almost like plain English: “wait until 200ms passes”.

For a visual representation, here is a small diagram:

flowchart LR
    A[code reaches the wait point] --> B["<-time.After(d)"]
    B -->|blocks| C[d elapses]
    C -->|signal in the channel| D[code continues execution]

2. time.After: a one-shot “alarm ring”

time.After(d) is the simplest “timer”: it says “when d has passed, a single event will arrive in the channel”. Usually we do not even store the channel in a variable: we just read from it and move on.

It is important to catch the nuance. time.Sleep(d) is “sleep for d”. time.After(d) is “receive an event after d”. At the code level, the result is often the same (you wait), but the “event-driven” model scales better: later, when select appears, you will be able to “wait for either a timer or something else”. Today we do not touch select, but we are already building the style.

The most honest example: wait and continue

Let’s start with the simplest possible code: wait for the event and continue.

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("start")                 // start
	<-time.After(200 * time.Millisecond) // wait for the time event
	fmt.Println("done")                  // done
}

The important point is that <-time.After(...) blocks execution just like Sleep, but psychologically you get used to the pattern “I am waiting for a signal”.

The timer returns time.Time: you can see when it fired

A time event is not just a “ping”. The channel receives a moment in time when the timer fired (type time.Time). This is sometimes useful for logs and diagnostics.

package main

import (
	"fmt"
	"time"
)

func main() {
	firedAt := <-time.After(100 * time.Millisecond)
	fmt.Println("timer fired at:", firedAt.Format(time.RFC3339Nano))
}

We are not going deep into formatting (that was a separate topic), but it is nice to understand: a timer is not “magic”, it is quite concrete.

Wait until a deadline: time.Until + time.After

A common task: we have a specific moment deadline, and we want to wait until it arrives. For that, it is convenient to compute the duration “how much time is left” and wait for it.

package main

import (
	"fmt"
	"time"
)

func main() {
	deadline := time.Now().Add(150 * time.Millisecond)
	<-time.After(time.Until(deadline))
	fmt.Println("deadline reached") // deadline reached
}

If the deadline has already passed, time.Until(deadline) will be negative. This is already a nuance: in real code, you typically check the sign and decide whether waiting makes sense at all.

3. time.NewTicker: regular “ticks” on a schedule

If time.After is “fire once”, then time.NewTicker(d) is “fire every d”. A ticker is useful when you need to do something regularly: print progress, refresh a cache, run a periodic status check, send a report, and so on.

In real systems, tickers show up very often: for example, periodic background operations may be driven by a ticker.

Technically, time.NewTicker(d) returns *time.Ticker, which has a field C — the event channel. We read from ticker.C the same way we read from time.After.

And right away, an important hygiene rule: you must stop a ticker. For that, there is ticker.Stop(). Usually this is done via defer, so you “definitely stop it” even if you leave the function early.

Three ticks — and that’s enough

package main

import (
	"fmt"
	"time"
)

func main() {
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for i := 1; i <= 3; i++ {
		<-ticker.C
		fmt.Println("tick", i) // tick 1, tick 2, tick 3
	}
}

Here you see the canonical pattern: create a ticker, defer Stop(), in a loop wait on <-ticker.C.

Mini app: a progress bar without a progress bar

Let’s start connecting examples into “one application” that evolves. Let’s say we have a small CLI program that simulates “long work” (for example, data processing) and prints progress at a fixed interval.

Yes, this is still a toy. But it trains really well: time measurement (time.Since) + periodic events (Ticker).

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	ticker := time.NewTicker(200 * time.Millisecond)
	defer ticker.Stop()

	for i := 0; i < 5; i++ {
		<-ticker.C
		fmt.Println("working, elapsed:", time.Since(start))
	}
}

If you run this, you will see “elapsed” growing while ticks arrive regularly.

A small fact “from the world of Go”: starting with Go 1.9, the time package transparently tracks the monotonic time component inside time.Time values, and therefore time.Since(start) remains correct even when the system clock shifts (within reason). This does not change your code, but it makes measurements more reliable.

Why a ticker is not perfectly precise

It is tempting to think of a ticker as Swiss clockwork. But it is more like a “good alarm” that may ring a bit later if the program is busy or the OS decided something else is more important right now.

In practice this means: a ticker is a regular signal, not a guarantee of microsecond-level accuracy. If you need “exactly every 10ms, strictly”, you are entering a territory where you must discuss load, the scheduler, priorities — and generally get a bit sad. We are not going there.

4. How to choose: Sleep, After, Ticker

When a beginner sees three tools, they often choose at random. To avoid guessing, it helps to keep a simple model in mind: what is one-shot, what is periodic, and what requires stopping.

Here is a small table that helps “put things on the shelves”:

Tool What it means How many events Do you need to stop it
time.Sleep(d)
“Pause in the current code” 0 (just a pause) no
time.After(d)
“An event after d” 1 no (in your code)
time.NewTicker(d)
“An event every d” many yes, ticker.Stop()

Why is Sleep not the same as After, even though both “wait”? Because After returns an event (a channel), and it is easier to combine with other waits when you get to more complex constructs. Today we do not use this, but we are already building the style.

5. Training app: waiting and periodic status

Now let’s take a step from “it just ticks” to a small utility where timers and tickers look like part of the program’s behavior.

Imagine our console app can “wait for the start”, and then “print status”. This can resemble a mini timer for a focus session: first we wait for the beginning, then every N seconds we print that we are still alive.

Let’s make two small functions so the code reads more easily. We have been able to write functions for a long time; we are just finally using them in everyday code.

Function: wait for the start via time.After

package main

import (
	"fmt"
	"time"
)

func waitStart(delay time.Duration) {
	fmt.Println("waiting:", delay) // waiting: 300ms
	<-time.After(delay)
	fmt.Println("go!") // go!
}

func main() {
	waitStart(300 * time.Millisecond)
}

Function: print status by ticker N times

package main

import (
	"fmt"
	"time"
)

func printStatusEvery(d time.Duration, times int) {
	ticker := time.NewTicker(d)
	defer ticker.Stop()

	for i := 1; i <= times; i++ {
		<-ticker.C
		fmt.Println("status tick:", i) // status tick: 1 ...
	}
}

func main() {
	printStatusEvery(200*time.Millisecond, 3)
}

Put it together: start in 0.5s, then 5 status updates

package main

import (
	"time"
)

func main() {
	waitStart(500 * time.Millisecond)
	printStatusEvery(300*time.Millisecond, 5)
}

Yes, here waitStart and printStatusEvery must be in the same file (or imported from a package, but we are not making packages more complex right now). The point is that the examples start to form one story: “we can wait for an event” and “we can do an action at an interval”.

Topic boundary: what we deliberately do not do today

It is very easy to “accidentally” fall into a more complex level. For example, you might want: “can I make the ticker tick, but stop it early based on a condition?” — and then concurrency and select almost inevitably appear.

Today we do not use select, do not start goroutines, and do not combine waiting on a ticker with waiting on something else. Our model is simple: reach the wait point → block → receive an event → move on. For a start, this is ideal: you understand the mechanics and do not drown in new concepts.

6. Typical mistakes when working with time.After and time.NewTicker

Mistake #1: forget that time.After and ticker.C are a “signal”, not a “pause function”.
Sometimes a student writes time.After(1 * time.Second) and expects the program to wait. But without reading via <- you just created a timer and threw it “into space”, and the code continued. The correct waiting form looks like <-time.After(d) or reading from a saved channel.

Mistake #2: create a ticker and not stop it.
A ticker is an object that lives and generates events. If you do not stop it, it will keep “ticking” even if you no longer need it. In a short program this may be unnoticeable, but in a long-running application it turns into a resource leak and strange background activity. The habit is simple: you created ticker := time.NewTicker(...) — on the very next line, mentally add defer ticker.Stop().

Mistake #3: do time.After inside a tight loop without understanding the consequences.
A pattern like “in each loop iteration, do <-time.After(10ms)” looks harmless, but it creates a new timer every time. Sometimes that is fine, but often it is better to use a single ticker. If you need a regular rhythm, a ticker is usually clearer and cheaper.

Mistake #4: expect perfect periodicity and zero drift.
If you print a message “every second”, that means “approximately every second”. Under load, with slow console output, with OS background tasks, ticks may arrive slightly later. This is not a “Go bug”, it is the reality of program execution. Treat a ticker as a regular signal, not as a lab-grade pulse generator.

Mistake #5: try to build an operation timeout with timers without changing function contracts.
Sometimes you want: “I will set time.After, and if the operation did not finish in time — it will get cancelled”. But waiting on a timer by itself cancels nothing: it only says “time is up”. For the operation to actually stop, you need an agreement in the code (usually via context and cancellation checks). That is a separate topic: do not confuse “waiting for a time event” with “being able to cancel work”.

1
Task
Go SELF, level 34, lesson 3
Locked
Short countdown
Short countdown
1
Task
Go SELF, level 34, lesson 3
Locked
Finish by the deadline
Finish by the deadline
1
Task
Go SELF, level 34, lesson 3
Locked
Service heartbeat
Service heartbeat
1
Task
Go SELF, level 34, lesson 3
Locked
Start and status
Start and status
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION