What is a goroutine, and how is it different from an OS thread?
Quick Answer
A goroutine is a lightweight, function-level unit of concurrent execution managed by the Go runtime, started with the go keyword: go doWork(). Unlike an OS thread, a goroutine starts with a tiny stack (about 2KB) that grows and shrinks dynamically, so you can run hundreds of thousands of goroutines cheaply, versus a few thousand OS threads at most. The Go runtime multiplexes many goroutines onto a much smaller number of OS threads (an M:N scheduler), parking goroutines that block on channel operations or I/O without blocking the underlying OS thread. This is what makes goroutines cheap enough to use liberally, for example spawning one per incoming request, where spawning a full OS thread per request would be far too expensive.
Detailed Answer
Goroutines are Go's core concurrency primitive, and their cost model is what makes Go's "just spawn a goroutine" style practical.
Starting one
func sayHello() {
fmt.Println("hello")
}
go sayHello() // runs concurrently, doesn't block the caller
time.Sleep(time.Millisecond) // without this, main might exit before sayHello runs
main exiting ends the whole program immediately, even if goroutines are still running. Real code coordinates completion with a sync.WaitGroup or a channel, not time.Sleep.
Why goroutines are cheap
| OS thread | Goroutine | |
|---|---|---|
| Initial stack size | Usually 1-8MB, fixed | ~2KB, grows/shrinks dynamically |
| Typical max count | A few thousand | Hundreds of thousands to millions |
| Scheduled by | The OS kernel | The Go runtime (in user space) |
| Context switch cost | Relatively expensive (kernel involved) | Much cheaper (userspace scheduler) |
The M:N scheduler
The Go runtime maps many goroutines (G) onto a smaller number of OS threads (M), coordinated through logical processors (P), roughly one per CPU core by default. When a goroutine blocks on a channel operation or a network call, the runtime parks it. It lets the OS thread run a different goroutine instead of sitting idle. This is why a Go program with 100,000 goroutines might only use a handful of OS threads under the hood.
Practical implication
Spawning a goroutine per incoming HTTP request, or per item in a batch job, is a completely normal Go pattern precisely because goroutines are this cheap. The same pattern with OS threads would exhaust system resources long before you got to that kind of concurrency.