Goroutines and Channels in Go Explained
A goroutine is a lightweight thread managed by the Go runtime. You can run hundreds of thousands of them. Channels let goroutines pass data safely.
Start goroutines and wait for them
var wg sync.WaitGroup
urls := []string{"https://go.dev", "https://pkg.go.dev", "https://example.com"}
for _, url := range urls {
wg.Go(func() { // Go 1.25+; earlier: wg.Add(1) + defer wg.Done()
resp, err := http.Get(url)
if err != nil {
log.Println(url, err)
return
}
resp.Body.Close()
log.Println(url, resp.Status)
})
}
wg.Wait()
Since Go 1.22 each loop iteration has its own url variable, so capturing it in a goroutine is safe.
Channels
results := make(chan string)
go func() {
results <- "done" // send (blocks until someone receives)
}()
msg := <-results // receive
A worker pool
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 4; w++ {
go func() {
for n := range jobs { // loops until jobs is closed
results <- n * n
}
}()
}
for i := 1; i <= 20; i++ {
jobs <- i
}
close(jobs)
for i := 0; i < 20; i++ {
fmt.Println(<-results)
}
select: wait on several channels
select {
case res := <-results:
fmt.Println("got", res)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
case <-ctx.Done():
return ctx.Err()
}
Avoid these mistakes
- Goroutine leaks: a goroutine blocked forever on a channel nobody reads. Always give goroutines a way to stop (a closed channel or a cancelled context).
- Data races: two goroutines writing the same variable. Run tests with
go test -race. - Only the sender should close a channel, and only once.
- For simple shared counters or maps, a
sync.Mutexis often clearer than channels.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.