Goroutines
A goroutine is a lightweight, concurrently executing function managed by the Go runtime. Goroutines are far cheaper than OS threads — you can run hundreds of thousands on a single machine.
Launching a Goroutine
Prefix any function call with go to run it concurrently:
package main
import (
"fmt"
"time"
)
func greet(name string) {
fmt.Println("Hello,", name)
}
func main() {
go greet("Alice")
go greet("Bob")
go greet("Charlie")
// Without this sleep, main may exit before the goroutines run
time.Sleep(10 * time.Millisecond)
fmt.Println("done")
}Idiomatic Go: Do not use
time.Sleepto wait for goroutines in real code — it is a race condition. Usesync.WaitGroupor channels instead.
sync.WaitGroup
WaitGroup coordinates the completion of a known number of goroutines:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signal completion when this function returns
fmt.Printf("worker %d starting\n", id)
// simulate work
fmt.Printf("worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // increment counter before launching goroutine
go worker(i, &wg)
}
wg.Wait() // block until counter reaches zero
fmt.Println("all workers finished")
}The Add(n) call must happen before the goroutine is launched (or at least before Wait is called). Done() decrements the counter; Wait() blocks until it reaches zero.
Goroutines and Closures
Goroutines capture variables by reference. A common mistake in loops:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// Bug: all goroutines may print the same value of i
// because i is shared by reference
// for i := 0; i < 5; i++ {
// wg.Add(1)
// go func() { defer wg.Done(); fmt.Println(i) }()
// }
// Fix: pass i as an argument to capture its value
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
}The Go Scheduler
Go uses an M:N scheduler — it multiplexes M goroutines onto N OS threads. The GOMAXPROCS environment variable (default: number of CPU cores) controls how many OS threads execute goroutines in parallel. You rarely need to change it.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("CPUs:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("Active goroutines:", runtime.NumGoroutine())
}Goroutine Leaks
A goroutine that is blocked forever (waiting on a channel that will never receive) is a goroutine leak. Always ensure goroutines can exit:
package main
import (
"context"
"fmt"
"sync"
)
func process(ctx context.Context, wg *sync.WaitGroup, items <-chan int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("worker cancelled")
return
case item, ok := <-items:
if !ok {
return
}
fmt.Println("processed:", item)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
items := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(1)
go process(ctx, &wg, items)
for i := 1; i <= 3; i++ {
items <- i
}
close(items)
wg.Wait()
}Key Takeaways
go f()launchesfas a goroutine; it is non-blocking.- Use
sync.WaitGroupto wait for goroutines to finish:Add(1)before launch,Done()at end,Wait()to block. - Pass loop variables as arguments to goroutine functions to avoid closure capture bugs.
- Goroutines are cheap — running thousands is normal; leaking them is a bug.
- Always give goroutines a way to exit (channels, contexts).