What does time.AfterFunc do, and on which goroutine does its function run?
answer
- no channel comes back at all
- the runtime calls you instead
- somebody has to run that function
- a brand-new goroutine at firing time
- Stop cancels the schedule, not the running call
basics
~20 stime.AfterFunc(d, f) schedules f to run once after duration d, on a new goroutine the runtime starts when the timer fires. It returns a *time.Timer used only for Stop and Reset; that timer's C field is nil.
solid answer
~40 s`time.AfterFunc(d, f)` arms a one-shot timer that, when it fires, starts a **new goroutine** running `f`. There is no channel to receive on — the returned `*time.Timer` exists so you can `Stop` or `Reset` the schedule, and its `C` field is nil. Two consequences matter in review. First, `Stop` returns false once the timer has already fired, and it does **not** wait for `f` to finish, so after `Stop` returns `f` may still be running; if you need to know it is done, `f` must signal that itself. Second, because `f` runs on an ordinary goroutine, everything it touches needs its own synchronisation, and an unrecovered panic inside it takes down the whole process. It is the right tool when the deadline action is a callback rather than something you want to `select` on.
code
go · 7 linest := time.AfterFunc(5*time.Second, func() {
// runs on a new goroutine when the timer fires
})
if !t.Stop() {
// already fired: the function may still be running
}go deeper
Recall the shape: it takes a duration and a function, runs the function once after that duration, and hands back a timer you can stop. Knowing it uses no channel is the key point.
Be ready to say that the callback runs on a freshly started goroutine, that the returned timer's C field is nil, and that Stop only cancels an unfired timer rather than waiting for a running callback.
Show the review instincts: the callback needs its own synchronisation and its own recover, and cancellation needs an explicit completion signal from the callback because Stop gives you none. Know when a timer channel in a select is the better shape.
Own the choice between callback-style deadlines and channel-style ones across a codebase, and the cost model behind it — many pending timers versus many parked goroutines — so teams are not each inventing their own delayed-action mechanism.
## The API ```go func AfterFunc(d Duration, f func()) *Timer ``` It schedules `f` to be called once, after `d` has elapsed. Unlike `time.After` and `time.NewTimer`, nothing is delivered on a channel: the runtime calls the function for you. The returned `*time.Timer` is a control handle only — the documentation is explicit that this timer's `C` field is not used and is nil, so a receive on it would block forever. ## Which goroutine runs the function When the timer expires, the runtime starts a **new goroutine** and calls `f` on it. It does not run on the goroutine that called `AfterFunc`, and it does not run on some internal runtime thread you must be gentle with. Three practical consequences: 1. **`f` runs concurrently with everything else.** Any state it reads or writes needs a mutex, an atomic, or a channel, exactly as for any other goroutine. This is a common source of races because the call site looks like a scheduling detail rather than a goroutine spawn. 2. **A panic in `f` is not contained.** An unrecovered panic on any goroutine terminates the whole program, and no other goroutine can recover it. If `f` does anything that might panic, it must `defer` its own `recover`. 3. **The function should be short.** A long-running `f` is just a long-running goroutine with a delayed start; if that is what you want, be explicit about it. ## Stop, and what it does not promise `Stop` returns `true` if it cancelled the timer before it fired and `false` if the timer had already fired or had already been stopped. When it returns `false`, `f` may be about to run, may be running right now, or may have finished — `Stop` does not wait for `f` to complete before returning. That is the trap: ```go t := time.AfterFunc(5*time.Second, func() { // runs on a new goroutine }) if !t.Stop() { // already fired: f may still be running } ``` If your cleanup path assumes that after `Stop` nothing else will touch a resource, that assumption is wrong. To get the guarantee you have to build it: have `f` close a channel or call `WaitGroup.Done` as its last act, and wait on that. `Reset(d)` re-arms the same timer to call `f` again after a new duration, which is how you push a deadline back without allocating a fresh timer. ## Why not just a goroutine and a sleep? `go func() { time.Sleep(d); f() }()` looks equivalent, and for one call it effectively is. The differences show up at scale and at cancellation time: - **Cost.** `AfterFunc` registers one runtime timer and only creates a goroutine when it fires. The sleep version creates the goroutine immediately, each with its own stack, and parks it for the whole duration. A large number of pending delayed actions is far cheaper as timers than as sleeping goroutines. - **Cancellation.** A sleeping goroutine cannot be woken early without extra plumbing; an `AfterFunc` timer has `Stop` and `Reset` built in. - **Rescheduling.** Pushing the deadline out is one `Reset` call rather than tearing down and recreating a goroutine. ## Where it fits `AfterFunc` suits fire-and-forget deadline actions: mark a pending item abandoned, flush a batch that has been idle too long, close a resource that was never claimed. It fits badly when the deadline needs to compete with other events — if you want "either a reading arrives or the deadline passes", you want a timer channel in a `select`, not a callback that has to synchronise its way back to the waiting goroutine. The failure mode to watch for in a long-lived agent is arming an `AfterFunc` per unit of work and never stopping the ones whose work finished early. Each pending callback is a scheduled action the runtime must still honour, and if your code also stores the returned `*time.Timer` in a map to be able to cancel it, that map is a retention point that grows with uptime unless entries are removed as well as stopped.
- How is time.AfterFunc cheaper than a goroutine that sleeps and then acts?`AfterFunc` registers a single runtime timer and only starts a goroutine when it fires, whereas the sleep version creates a goroutine with its own stack up front and parks it for the whole duration. With many pending delayed actions that difference is substantial. `AfterFunc` also gives you `Stop` and `Reset`, while a sleeping goroutine cannot be cancelled or rescheduled without extra machinery.
- Can you receive from the returned Timer's C field to learn when the function ran?No. For a timer created by `time.AfterFunc` the `C` field is nil, so a receive on it blocks forever — the value is delivered as a function call, not on a channel. If you need to know the callback finished, have the function itself signal: close a channel, call `WaitGroup.Done`, or set an atomic as its last statement.
- What happens if the scheduled function panics?It runs on an ordinary goroutine, so an unrecovered panic there crashes the entire process — no other goroutine can recover a panic it did not raise, and the `AfterFunc` call site is long gone by then. If the callback does anything that might panic, it must defer its own recover inside the function body.
saying these in an interview costs you the question
- Thinks the function runs on the goroutine that called AfterFunc
- Assumes Stop waits for the callback to finish
- Tries to receive from the returned Timer's C field
- Expects a panic in the callback to be swallowed
- Treats the callback as single-threaded and skips synchronisation