Lesson 2 / 25

Goroutines and Their Lifecycle

Starting is easy; knowing who stops a goroutine is the real design question.

Every goroutine needs an exit

The go keyword starts a function call in a new goroutine and returns immediately; there is no handle to join or kill it. A goroutine ends only when its function returns (or the whole program exits: when main returns, all other goroutines are abandoned mid-flight). Goroutines start with small, growable stacks, so creating thousands is normal, but each one still holds memory and anything it references until it returns. Therefore, whenever you write go, answer two questions: how will this goroutine stop, and who waits for it to finish? Typical answers are a closed channel, a cancelled context.Context, and a sync.WaitGroup or errgroup.Group that the caller waits on.

A goroutine with an explicit stop signal

The caller owns both the stop channel and the wait.

package main

import (
	"fmt"
	"sync"
	"time"
)

func heartbeat(stop <-chan struct{}, wg *sync.WaitGroup) {
	defer wg.Done()
	t := time.NewTicker(100 * time.Millisecond)
	defer t.Stop()
	for {
		select {
		case <-stop:
			fmt.Println("heartbeat: stopping")
			return
		case <-t.C:
			fmt.Println("beat")
		}
	}
}

func main() {
	stop := make(chan struct{})
	var wg sync.WaitGroup
	wg.Add(1)
	go heartbeat(stop, &wg)

	time.Sleep(350 * time.Millisecond)
	close(stop) // broadcast "stop" to the goroutine
	wg.Wait()   // and wait until it has actually returned
}

Never fix ordering with time.Sleep

Sleeping in main "to let goroutines finish" is a race that happens to pass on your machine. Use a WaitGroup, a channel or errgroup to wait for completion explicitly.

Quick check: What happens to running goroutines when main returns?

  • The program exits and they are stopped without running further code
  • main waits for them automatically
  • They keep running in the background as a daemon
  • The runtime panics with a deadlock error
Answer

The program exits and they are stopped without running further code — Program exit does not wait for goroutines and does not run their deferred calls; you must wait for them explicitly.