Goroutines

A goroutine is a function running concurrently with the rest of your program. Starting one is as simple as writing go before a function call, and they're so lightweight that a program can run hundreds of thousands at once. This lesson covers starting goroutines, waiting for them with sync.WaitGroup, why output order isn't guaranteed, and the rules for goroutines that panic or never finish.

Starting a goroutine

go f(x) starts f(x) running in a new goroutine and carries on immediately, without waiting for it to finish. When main returns, the program ends — even if other goroutines are still running — so you need a way to wait:

GO
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	wg.Go(func() {
		fmt.Println("hello from a goroutine")
	})

	wg.Wait() // block until every goroutine started with wg.Go has finished
	fmt.Println("main is done")
}
Output
hello from a goroutine
main is done

wg.Go(f) (added in Go 1.25) starts f in a new goroutine and tracks it; wg.Wait() blocks until all tracked goroutines have returned. Without the Wait, main would probably finish before the goroutine got a chance to print anything.

Many goroutines at once

Here five slow tasks run concurrently. Each takes about 100 milliseconds, but because they run at the same time, the whole program takes about 100 milliseconds rather than 500:

GO
package main

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

func fetch(id int) string {
	time.Sleep(100 * time.Millisecond) // pretend to call a slow service
	return fmt.Sprintf("result %d", id)
}

func main() {
	start := time.Now()
	results := make([]string, 5)

	var wg sync.WaitGroup
	for i := range 5 {
		wg.Go(func() {
			results[i] = fetch(i) // each goroutine writes its own slot
		})
	}
	wg.Wait()

	fmt.Println(results)
	fmt.Println("took less than 300ms:", time.Since(start) < 300*time.Millisecond)
}
Output
[result 0 result 1 result 2 result 3 result 4]
took less than 300ms: true

Each goroutine writes to a different element of results, so they don't interfere. Since Go 1.22, each loop iteration has its own i, so every closure sees the right index.

Order is not guaranteed

The Go scheduler decides when each goroutine runs, and on a multi-core machine many run in parallel. If goroutines print directly, the order can change from run to run:

GO
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := range 4 {
		wg.Go(func() { fmt.Println("worker", i) })
	}
	wg.Wait()
}

One run might print workers 3, 0, 1, 2; the next 0, 2, 3, 1. Never rely on the order goroutines run in. When order matters, collect results into indexed slots as above, or send them over a channel and sort them (next lesson).

The older WaitGroup style

Before wg.Go, you called wg.Add(1) before starting each goroutine and defer wg.Done() inside it. You'll see this in a lot of existing code, and it still works:

GO
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	squares := make([]int, 4)
	for i := range 4 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			squares[i] = i * i
		}()
	}
	wg.Wait()
	fmt.Println(squares)
}
Output
[0 1 4 9]

The Add must happen before the go statement, not inside the goroutine, or Wait might return before the goroutine has even registered. wg.Go removes that trap, and go fix can rewrite the old pattern to the new one.

Goroutines are cheap

A goroutine starts with a stack of a few kilobytes that grows only if needed, and the runtime multiplexes them onto a small number of operating-system threads. Starting 100,000 is no problem:

GO
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var wg sync.WaitGroup
	var total atomic.Int64
	for i := range 100_000 {
		wg.Go(func() { total.Add(int64(i)) })
	}
	wg.Wait()
	fmt.Println(total.Load())
}
Output
4999950000

atomic.Int64 lets all those goroutines add to one number safely. Shared data needs this kind of protection — lesson 54 explains why.

A panic in a goroutine crashes the program

A panic that isn't recovered inside the goroutine where it happened ends the entire program, not just that goroutine. main can't catch it. If a goroutine might panic and the program must survive, recover inside the goroutine itself (lesson 46).

Don't leak goroutines

A goroutine that blocks forever — waiting on a channel nobody will ever send to, say — is never cleaned up. Such leaked goroutines slowly use up memory in long-running programs. Every goroutine you start should have a clear way to finish; the next lessons show the tools for that, and Go 1.27's goroutineleak profile can help find leaks in running programs.

Concurrency versus parallelism

Concurrency is structuring a program as independent tasks; parallelism is actually running them at the same instant on several CPU cores. Goroutines give you concurrency; the runtime gives you parallelism automatically, using as many cores as GOMAXPROCS allows (by default, the number of CPUs available to the program).

Try it yourself

Check the sizes of several "pages" concurrently and print them in the original order.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
home       400 bytes
about      500 bytes
contact    700 bytes
blog       400 bytes
Next: passing data between goroutines with channels.