Notessh2a
Concurrency

Goroutines

Overview

Goroutines are lightweight units of execution managed by the Go runtime scheduler to run functions concurrently.

In general, program execution can be divided into two types of routines:

  • Main Routine: The first goroutine started by a Go program. It executes the main() function in the main package and defines the lifetime of the program. When the main routine finishes, the program exits immediately, even if other goroutines are still running.
  • Child Routine: Any goroutine started from another routine using the go keyword. It runs concurrently with the parent routine and performs work independently. It does not block the parent unless synchronization is explicitly implemented. It completes when its function returns or when the program terminates.
func main() {
	go expensiveFunc("Hello") // Run expensiveFunc concurrently (don't wait for it)

	fmt.Println("Main")

	time.Sleep(1700 * time.Millisecond)
}

func expensiveFunc(text string) {
	for i := 0; i < 4; i++ {
		time.Sleep(500 * time.Millisecond)
		fmt.Println(text, i)
	}
}

// Main
// Hello 0
// Hello 1
// Hello 2

The time.Sleep in the main function is used only to give the goroutine enough time to run before the main routine exits. Without it, the program would terminate as soon as the main routine completes.

The output shows only 3 iterations even though the loop is set for 4 iterations. This is because time.Sleep of 1700 ms is less than the 4 * 500 ms required for all iterations of expensiveFunc. Meaning only the first 3 iterations had enough time to complete before the program exited.

WaitGroup

In Go, a WaitGroup from the sync package is used to wait for a collection of goroutines to complete before continuing execution in the main routine.

How it works:

  • Add(n) increments the counter by n before starting the goroutines.
  • Each goroutine calls Done() when it completes. This decrements the counter by 1.
  • Wait() blocks the parent routine until the counter reaches zero.
func main() {
	var wg sync.WaitGroup

	wg.Add(1)
	go expensiveFunc("Hello", &wg)

	fmt.Println("Main")

	wg.Wait()

	fmt.Println("End")
}

func expensiveFunc(text string, wg *sync.WaitGroup) {
	defer wg.Done()

	for i := range 4 {
		time.Sleep(500 * time.Millisecond)
		fmt.Println(text, i)
	}
}

// Main
// Hello 0
// Hello 1
// Hello 2
// Hello 3
// End

The business logic can be wrapped inside an anonymous function to keep it isolated:

func main() {
	var wg sync.WaitGroup

	wg.Add(1)
	go func(wg *sync.WaitGroup) {
		defer wg.Done()
		expensiveFunc("Hello")
	}(&wg)

	fmt.Println("Main")

	wg.Wait()

	fmt.Println("End")
}

func expensiveFunc(text string) {
	for i := range 4 {
		time.Sleep(500 * time.Millisecond)
		fmt.Println(text, i)
	}
}

This approach keeps expensiveFunc function clean and focused on its logic, while goroutine management remains separate.

Mutexes

In Go, a Mutex (Mutual Exclusion) from the sync is a locking mechanism that allows only one goroutine at a time to read or modify shared data, forcing others to wait until it is free. This prevents race conditions and keeps the data consistent.

How it works:

  • Lock() attempts to acquire the mutex.
    • If the mutex is free, the goroutine acquires it and continues.
    • If the mutex is locked, the goroutine is blocked until it becomes available.
  • Unlock() releases the mutex and allows one waiting goroutine to acquire it.

Only one goroutine can hold the lock at a time. The mutex is not tied to any specific goroutine. Any goroutine can compete to acquire it when it becomes available.

Calling Unlock() on an unlocked mutex causes a panic.

On this page