Goroutines vs threads: 8 differences that matter
Why Go can run a hundred thousand goroutines when the same number of OS threads would sink your machine.
- Explain the 8 differences between a goroutine and an OS thread in an interview.
- Describe how the Go scheduler runs many goroutines on a few threads, and how it preempts them.
- Run a program that starts 100,000 goroutines and prints the result.
A goroutine is a function run concurrently by the Go runtime. An OS thread is run by the operating system kernel. Go runs many goroutines on a few threads. Here is the whole comparison in one table. Each row is explained below.
| Goroutine | OS thread | |
|---|---|---|
| Managed by | Go runtime (user space) | Operating system kernel |
| Starting stack | Small (2 KB), grows and shrinks as needed | Fixed, usually 1–8 MB reserved |
| Cost to create | Very cheap, no system call | Expensive, needs a system call |
| Switching cost | Cheap, done by the Go scheduler | Costlier, done by the kernel |
| Scheduling | Go scheduler, M:N (many goroutines on N threads), preemptive since Go 1.14 | Kernel scheduler, preemptive |
| How many you can run | Hundreds of thousands is normal | Usually thousands before memory hurts |
| Identity | No ID you can get at; on purpose | Has a thread ID |
| Usual way to talk | Channels (or a mutex when simpler) | Shared memory plus locks |
What is the difference between a goroutine and a thread?
A thread is scheduled by the operating system and costs megabytes of reserved stack. A goroutine is scheduled by the Go runtime, starts with a 2 KB stack, and many goroutines share a few threads. So goroutines are far cheaper to create and switch.
You still get real parallelism: the runtime uses as many threads for Go code as GOMAXPROCS says (by default, the number of CPU cores). The basics are in the Goroutines lesson (Goroutines).
Are goroutines cooperative or preemptive?
Preemptive. Since Go 1.14 the runtime has asynchronous preemption: it can stop a goroutine that runs too long (about 10 ms), even a tight loop with no function calls, by sending its thread a signal. Before 1.14 it was mostly cooperative.
Some older articles (including a well-known GeeksforGeeks page) still say goroutines use "cooperative scheduling". That was true of old Go versions, not of any Go you will install today. If an interviewer says it, you can politely mention Go 1.14.
package mainimport ( "fmt" "runtime" "time")func main() { runtime.GOMAXPROCS(1) // only one thread runs Go code go func() { for { // a busy loop that never calls a function or blocks } }() time.Sleep(20 * time.Millisecond) // Before Go 1.14 this line could hang forever: the loop never gave up the thread. fmt.Println("main still runs: the busy goroutine was preempted")}Why are goroutines so cheap?
Because they start with a tiny stack that grows only when needed, and creating or switching one never asks the kernel. A thread reserves a big stack up front and every switch goes through the kernel. That is why 100,000 goroutines is fine.
package mainimport ( "fmt" "sync" "sync/atomic")func main() { var wg sync.WaitGroup var total atomic.Int64 for i := 1; i <= 100_000; i++ { wg.Add(1) go func() { defer wg.Done() total.Add(int64(i)) }() } wg.Wait() fmt.Println("goroutines started: 100000") fmt.Println("sum of 1..100000 =", total.Load())}How does the Go scheduler work (GMP)?
The scheduler matches three things: G (goroutines), M (OS threads) and P (processors, one per GOMAXPROCS). Each P has a queue of runnable Gs and runs them on an M. Idle Ps steal work from busy ones.
When a goroutine blocks on a channel, a mutex or the network, the runtime parks it and runs another G on that thread. When it blocks in a system call, the P is handed to another thread so other goroutines keep running.
Why does Go not give goroutines an ID?
On purpose, so people do not build "thread-local storage" on top of goroutine IDs. In Go you pass what a goroutine needs as arguments, or in a context.Context for request-scoped values (Context).
When are threads still the answer?
In Go, almost never directly. You write goroutines and let the runtime choose threads. runtime.LockOSThread exists for the rare case where a C library or OS API needs the same thread every time, such as some GUI or graphics libraries.
- Goroutines are run by the Go runtime; threads by the OS.
- Goroutines start at 2 KB of stack and grow; threads reserve megabytes.
- The scheduler runs many goroutines on GOMAXPROCS threads (M:N, the GMP model).
- Goroutines are preemptive since Go 1.14, even in tight loops. "Cooperative" is outdated.
- There is no goroutine ID; pass data in arguments or a context.
- 1Run
preempt.gowithruntime.GOMAXPROCS(1). Why does main still print? - 2Change
many.goto printruntime.NumGoroutine()just beforewg.Wait(). Why does the number change from run to run? - 3In one sentence each, name three things that make a goroutine cheaper than a thread.