Goroutines

intermediate go concurrency

A goroutine is a function that runs concurrently with other code. We start one by slapping the word go in front of a function call. That’s it.

In simple language, a goroutine is like a super-cheap thread we can spin up with one keyword.

go doSomething() // runs doSomething() concurrently, doesn't wait

The line above kicks off doSomething() and immediately moves on. We don’t block, we don’t wait — the goroutine does its thing in the background.

Why goroutines are special

Normal OS threads are expensive. Each one grabs around 1MB of stack memory and the operating system has to schedule them. Spin up thousands and your machine starts sweating.

Goroutines are different. Each one starts with only about 2KB of stack, and that stack grows and shrinks as needed. So we can happily run tens of thousands of goroutines on a normal machine.

Think of it like this: an OS thread is renting a big apartment, a goroutine is crashing on a friend’s couch. Way cheaper to have a lot of them.

The other trick is that goroutines don’t map one-to-one onto OS threads. The Go runtime scheduler multiplexes many goroutines onto a small number of OS threads. We’ll cover the scheduler in detail elsewhere — for now, just know the runtime is the smart middleman juggling everything.

Go runtime scheduler multiplexes goroutines onto OS threads
G1 G2 G3 G4 G5 ... thousands
↓ scheduler ↓
OS Thread 1 OS Thread 2

A main that exits kills everything

Here’s a gotcha that bites everyone once. When main() returns, the whole program exits — and any goroutines still running get killed instantly. No mercy.

package main

import "fmt"

func main() {
	go fmt.Println("hello from goroutine") // might never print!
	// main returns immediately, program dies before the goroutine runs
}

This program usually prints nothing. The goroutine never gets a chance to run because main exits first. We need to synchronize — wait for goroutines to finish using a sync.WaitGroup or a channel. (We cover both soon.)

The classic loop variable gotcha

This one shows up in interviews all the time. Before Go 1.22, the loop variable was shared across iterations.

// Go 1.21 and earlier — BUGGY
for _, v := range []string{"a", "b", "c"} {
	go func() {
		fmt.Println(v) // all three goroutines often print "c"
	}()
}

Why? All the goroutines closed over the same v variable. By the time they ran, the loop had finished and v held the last value. Classic.

The old fix was to pass it in or shadow it:

for _, v := range []string{"a", "b", "c"} {
	v := v // make a fresh copy per iteration
	go func() { fmt.Println(v) }()
}

Good news: Go 1.22 changed loop semantics so each iteration gets a fresh variable. So in modern Go the buggy version works fine. But interviewers love asking about it, so know both the bug and the fix.

Interview soundbite

A goroutine is a lightweight, runtime-managed thread started with the go keyword — it begins with about a 2KB growable stack, so we can run tens of thousands of them. The Go scheduler multiplexes them onto a few OS threads. Two classic gotchas: main exiting kills all goroutines (so we must synchronize), and the pre-Go-1.22 loop variable capture bug where goroutines shared one variable.