Goroutines are the reason Go exists as a language. Google built Go specifically because they needed lightweight concurrencyWhat is concurrency?The ability of a program to handle multiple tasks at the same time, like serving thousands of users without slowing down. that didn't require the ceremony of Java threads or the callbackWhat is callback?A function you pass into another function to be called later, often when an operation finishes or an event occurs. hell of Node.js. Every serious Go program uses goroutines, and every AI tool generates them, often incorrectly.
What a goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). actually is
A goroutine is not an OS thread. It's a function executing concurrently, managed entirely by the Go runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary.'s scheduler. The runtime multiplexes potentially millions of goroutines onto a small number of OS threads using an M:N scheduling model.
When you ask AI to "make this concurrent," it will add the go keyword. What it often won't do is think about what happens after.
AI will generate something like:
func main() {
go doWork()
fmt.Println("done")
}This program almost certainly prints "done" and exits before doWork runs at all. The main goroutine doesn't wait. When main returns, the entire process terminates, all goroutines are killed mid-execution with no cleanup.
| Concept | OS thread | Goroutine |
|---|---|---|
| Stack size | ~1MB fixed | ~2KB, grows dynamically |
| Creation cost | Expensive (syscall) | Cheap (runtime allocation) |
| Scheduling | OS kernel | Go runtime (user-space) |
| Context switch | Slow (~1-10us) | Fast (~200ns) |
| Practical limit | Thousands | Millions |
| Managed by | Operating system | Go scheduler (GOMAXPROCS threads) |
Launching goroutines
The go keyword before any function call launches that function in a new goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack).:
go myFunction() // named function
go obj.Method() // method call
go func() { // anonymous function (closure)
fmt.Println("inline")
}()The calling goroutine continues immediately, it does not block. There is no guarantee about execution order between goroutines.
go someFunction() without any mechanism to wait for completion or collect results. If you see a bare go call with no WaitGroup, channel, or context nearby, the goroutine's work may silently disappear.Waiting with sync.WaitGroup
The correct way to wait for goroutines is sync.WaitGroup:
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // blocks until all Done() calls match Add() calls
fmt.Println("All workers complete")
}The protocolWhat is protocol?An agreed-upon set of rules for how two systems communicate, defining the format of messages and the expected sequence of exchanges. is strict:
- Call
wg.Add(n)before launching the goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). - Call
wg.Done()inside the goroutine (always viadefer) - Call
wg.Wait()to block until the counter hits zero
wg.Add(1) inside the goroutine instead of before launching it. This creates a race condition, wg.Wait() might return before Add is called, because the goroutine hasn't been scheduled yet.// WRONG - AI generates this regularly
for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // race: Wait() might win
defer wg.Done()
process(id)
}(i)
}
wg.Wait()
// CORRECT - Add before go
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
process(id)
}(i)
}
wg.Wait()The loop variable capture bug
This is the single most frequent bug in AI-generated concurrent Go code. Before Go 1.22, loop variables were shared across iterations:
// BUGGY (Go < 1.22) - AI generates this constantly
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // all goroutines likely print 3
}()
}All goroutines capture the same i variable. By the time they execute, the loop has finished and i is 3. The fix is to pass i as an argument:
// CORRECT - parameter creates a copy
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // each goroutine gets its own copy
}(i)
}Go 1.22+ changed loop variable semantics so each iteration gets a fresh variable. But AI trained on older code still generates the buggy pattern, and you'll encounter pre-1.22 codebases constantly.
int loops but not for range loops over slices, or fixing the index but not a second captured variable. Check every closure inside a loop.| Go version | for i := 0 behavior | for i, v := range behavior |
|---|---|---|
| < 1.22 | i shared across iterations | Both i and v shared |
| >= 1.22 | Fresh i per iteration | Fresh i and v per iteration |
| AI default | Assumes old behavior (safer) | Inconsistent |
GoroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). leaks
A goroutine that can never exit is a leak. Unlike memory, leaked goroutines consume stack space, hold references to heap objects, and may hold locks or file handles indefinitely.
// LEAK - goroutine blocks forever
func fetchData() {
ch := make(chan string)
go func() {
result := <-ch // nothing ever sends on ch
process(result)
}()
// function returns, goroutine lives forever
}The question to always ask when reviewing AI-generated goroutines: "Under what conditions does this goroutine exit?" If there's no clear answer, no channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). close, no context cancellation, no return path, it's a leak.
r.Context() so goroutines respect request cancellation.The Go scheduler
You don't need to manage the scheduler, but understanding it helps you evaluate AI's concurrencyWhat is concurrency?The ability of a program to handle multiple tasks at the same time, like serving thousands of users without slowing down. choices.
The scheduler has three key entities: G (goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack).), M (OS thread), and P (processor/execution context). GOMAXPROCS controls how many P's exist (defaults to number of CPU cores). Each P can run one G at a time on one M.
When a goroutine makes a blocking syscall (file I/O, network), the scheduler detaches it from the P and assigns another goroutine. When a goroutine does channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). operations or calls runtime.Gosched(), it yields to the scheduler cooperatively.