LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 3219 min read

Goroutines

Learn how the go keyword launches lightweight goroutines, how they compare to OS threads, and how to run concurrent work with a basic example.

Introduction

Concurrency is one of Go's headline features, and it starts with the goroutine — a function that runs independently, at the same time as other goroutines, managed entirely by the Go runtime rather than the operating system. Goroutines are what make it practical to write programs that juggle thousands of simultaneous tasks without drowning in complexity.

What You Will Learn
  • What a goroutine is and how it differs from a regular function call.
  • How the go keyword launches a goroutine.
  • Why goroutines are dramatically lighter-weight than OS threads.
  • How to run a simple concurrent example.
  • Why a program can exit before its goroutines finish.

What Is a Goroutine?

A goroutine is a function that runs concurrently with the rest of your program, managed by the Go runtime scheduler rather than the operating system. Every Go program already has at least one goroutine — the one running func main(). Any additional goroutines you launch run alongside it.

Launching Goroutines with go

To start a new goroutine, put the go keyword in front of a function call. The call returns immediately, and the function body runs concurrently in the background.

package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("hello from a goroutine")
}
func main() {
go sayHello()
time.Sleep(100 * time.Millisecond) // give the goroutine time to run
fmt.Println("hello from main")
}
Output

Click Run to see what this code prints.

That time.Sleep is a crude way to wait for the goroutine — in real programs you would use a sync.WaitGroup or a channel instead (covered in upcoming lessons), because sleeping for a fixed duration is unreliable and slow.

Goroutines vs OS Threads

An OS thread is a relatively heavyweight resource: it typically reserves a megabyte or more of stack space and is scheduled by the operating system kernel. A goroutine, by contrast, starts with a stack of only a few kilobytes that grows and shrinks as needed, and it is scheduled cooperatively by the Go runtime on top of a small pool of OS threads. This means a single Go program can comfortably run tens or hundreds of thousands of goroutines at once.

AspectOS ThreadGoroutine
Initial stack size~1 MB (fixed)~2 KB (grows dynamically)
Created/destroyed byOperating systemGo runtime
Typical count per programHundredsThousands to millions
SchedulingPreemptive, kernel-managedCooperative, runtime-managed (multiplexed onto OS threads)

A Basic Concurrent Example

Here is a slightly larger example that launches several goroutines to simulate downloading files concurrently, using a sync.WaitGroup so main waits for all of them to finish before exiting (WaitGroup itself is covered in depth in the sync package lesson).

package main
import (
"fmt"
"sync"
)
func download(name string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("downloading %s...\n", name)
fmt.Printf("finished %s\n", name)
}
func main() {
files := []string{"a.zip", "b.zip", "c.zip"}
var wg sync.WaitGroup
for _, f := range files {
wg.Add(1)
go download(f, &wg)
}
wg.Wait()
fmt.Println("all downloads complete")
}
Output (order of the three downloads may vary)

Click Run to see what this code prints.

Because goroutines run concurrently, the interleaving of "downloading..." and "finished..." lines across different files is not guaranteed — only that "all downloads complete" prints last, since wg.Wait() blocks until every goroutine has called wg.Done().

Why main Can Exit Too Early

A Go program ends the instant func main() returns, regardless of whether any goroutines it launched are still running. This is one of the most common early surprises for newcomers: a goroutine you started may simply never get the chance to print anything if main exits first.

package main
import "fmt"
func main() {
go fmt.Println("might never print!")
// main returns immediately — the goroutine may not run at all
}
No Output Guaranteed

Running the snippet above often prints nothing at all, because main() can finish before the scheduler even gets a chance to run the goroutine. Always synchronize with a WaitGroup or channel instead of relying on timing.

Common Mistakes

Avoid These Mistakes
  • Letting main() return before goroutines finish, silently dropping their work.
  • Using time.Sleep as a substitute for proper synchronization.
  • Capturing a loop variable by reference inside a goroutine closure without passing it as a parameter.
  • Assuming goroutines run in the order they were launched — they do not.
  • Spawning unbounded goroutines for unbounded input, exhausting memory.

Best Practices

  • Always have a clear way to wait for goroutines to finish (WaitGroup or channel).
  • Pass loop variables into a goroutine as function parameters to avoid capture bugs.
  • Keep goroutines focused on a single, well-defined task.
  • Limit concurrency with a worker pool or buffered channel when processing large or unbounded input.
  • Treat goroutine panics carefully — an unrecovered panic in a goroutine crashes the whole program.

Frequently Asked Questions

Practically, tens of thousands to millions, limited mainly by available memory, since each goroutine starts with only a few kilobytes of stack.

Both are possible. On a multi-core machine, the Go runtime can run goroutines truly in parallel across multiple OS threads; on a single core they are interleaved concurrently.

The entire program crashes, even if other goroutines are still running. Recovering panics inside long-lived goroutines is an important defensive pattern.

Key Takeaways

  • A goroutine is a lightweight, independently running function managed by the Go runtime.
  • The go keyword launches a goroutine and returns immediately.
  • Goroutines use far less memory than OS threads, so you can run vastly more of them.
  • main() exiting stops the whole program, even if goroutines are still working.
  • Proper synchronization (not time.Sleep) is required to wait for goroutines to finish.

Summary

Goroutines are the foundation of Go's concurrency model: cheap, easy to launch with the go keyword, and scheduled efficiently by the runtime. The tricky part is not starting them — it is coordinating them safely, which is exactly what channels, covered next, are designed to do.

Next Lesson →

Channels