Skip to content
AgileLogics
7 min read

Goroutines

Prerequisites: Generics

A goroutine is a function running concurrently with the rest of your program. Start one by putting go in front of a function call. Goroutines are managed by the Go runtime, not the operating system; they start with a small stack, so a program can run thousands of them.

Two facts matter from the start:

  1. When main returns, the program exits — running goroutines are killed. You must explicitly wait for work you care about.
  2. The scheduler decides when each goroutine runs. You cannot assume any order between them unless you synchronise.

Syntax

go doWork(42)

go func() {
    fmt.Println("anonymous goroutine")
}()

The problem: main doesn't wait

package main

import "fmt"

func main() {
    go fmt.Println("from goroutine")
    fmt.Println("from main")
}

This output is not deterministic. Usually you see only from main, because main returns before the goroutine gets a chance to run.

Example: waiting with sync.WaitGroup

A sync.WaitGroup counts outstanding goroutines: Add(1) before starting one, Done() when it finishes, and Wait() blocks until the count is zero. (The sync lesson covers it in detail.)

package main

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

func slowSquare(n int) int {
    time.Sleep(100 * time.Millisecond) // pretend this is slow work
    return n * n
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    results := make([]int, len(nums))

    start := time.Now()
    var wg sync.WaitGroup
    for i, n := range nums {
        wg.Add(1)
        go func() {
            defer wg.Done()
            results[i] = slowSquare(n) // each goroutine writes only its own index
        }()
    }
    wg.Wait()

    fmt.Println(results)
    fmt.Println("elapsed:", time.Since(start).Round(100*time.Millisecond))
}

Expected output

[1 4 9 16 25]
elapsed: 100ms

The five 100 ms tasks ran concurrently, so the total is roughly 100 ms rather than 500 ms (the exact elapsed time varies slightly between runs). The results are in order because each goroutine writes to its own slice index — the goroutines themselves may finish in any order.

Because Go 1.22+ creates new i and n variables for every loop iteration, each closure sees its own values.

Common mistakes

  • Letting main return before goroutines finish, or "fixing" it with time.Sleep. Sleeping is a guess; use a WaitGroup or channels.
  • Having several goroutines write the same variable (for example total += n) without synchronisation. That is a data race: the result is unpredictable. Run go run -race . or go test -race to detect it.
  • Calling wg.Add(1) inside the goroutine. Wait may run before the Add and return too early. Call Add before the go statement.
  • Starting goroutines that can never finish (a goroutine leak). Every goroutine you start should have a clear way to end.

Practice

Split nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} into three chunks of three. Sum each chunk in its own goroutine, storing the result in sums[i], wait for all of them, then print sums and the grand total.

Show solution
package main

import (
    "fmt"
    "sync"
)

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    const chunks = 3
    size := len(nums) / chunks
    sums := make([]int, chunks)

    var wg sync.WaitGroup
    for i := range chunks {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for _, n := range nums[i*size : (i+1)*size] {
                sums[i] += n // only this goroutine touches sums[i]
            }
        }()
    }
    wg.Wait()

    total := 0
    for _, s := range sums {
        total += s
    }
    fmt.Println(sums, total) // [6 15 24] 45
}