Go Concurrency: Making Your Programs Do Multiple Things at Once

The Coffee Shop That Changed Everything

Picture this: You walk into a coffee shop with a single barista. There's a line of 50 customers. Each coffee takes 3 minutes to make. Simple math tells you the last person waits 150 minutes. That's over two hours for a coffee!
Now imagine the same shop with 10 baristas working simultaneously. Suddenly, 10 customers get served every 3 minutes. The wait drops dramatically. This is exactly what concurrency does for your programs.
Your Go program might be that single barista right now, handling one task while everything else waits. Let's change that.

Why Traditional Programs Feel Slow

Before concurrency became easy to use, programs ran like that single barista. One task. Then the next. Then the next.
Task 1 → Complete → Task 2 → Complete → Task 3 → Complete
This worked fine when computers did simple things. But modern applications need to:
  • Download files while updating the UI
  • Handle thousands of web requests at once
  • Process data streams continuously
  • Talk to multiple databases simultaneously
The old way? Everything queues up. Your user stares at a loading screen. Your server chokes under load.
Go blog diagram 1

Go blog diagram 1

Each request waits for the previous one to complete. If processing takes 100ms each, three requests take 300ms total. Scale that to 10,000 requests and you're looking at 16 minutes of processing that could happen in milliseconds.

Think Like a Restaurant Kitchen

Think of it like this: A busy restaurant kitchen isn't one chef doing everything. It's multiple cooks working on different dishes at the same time. One prepares appetizers, another handles the main course, a third works on desserts. They share resources like the stove and refrigerator, but work independently on their tasks.
In Go, goroutines are your cooks. They're lightweight workers that run concurrently. The Go runtime is the kitchen manager, making sure everyone gets time with the stove without chaos.
The beauty? You don't manage the complexity. You just say "start cooking this dish" and Go handles the rest.

Goroutines: Your Lightweight Workers

A goroutine is simply a function that runs independently of the code that started it. Creating one takes a single keyword: go.
Go blog diagram 2

Go blog diagram 2

Here's the magic in action:
go
// Filename: goroutine_basics.go package main import ( "fmt" "time" ) // brewCoffee simulates making a coffee order // Why: Demonstrates a task that takes time to complete func brewCoffee(order string) { fmt.Printf("Starting: %s\n", order) time.Sleep(2 * time.Second) // Simulates brewing time fmt.Printf("Completed: %s\n", order) } func main() { orders := []string{"Latte", "Espresso", "Cappuccino", "Americano"} start := time.Now() // Start each order as a separate goroutine // Why: Each barista (goroutine) works independently for _, order := range orders { go brewCoffee(order) // The 'go' keyword creates a goroutine } // Wait for all goroutines to finish // Why: main() exits immediately without this, killing all goroutines time.Sleep(3 * time.Second) fmt.Printf("All orders completed in: %v\n", time.Since(start)) }
Expected Output:
Starting: Americano Starting: Latte Starting: Cappuccino Starting: Espresso Completed: Latte Completed: Cappuccino Completed: Americano Completed: Espresso All orders completed in: 3.001s
Notice something powerful? Four 2 second tasks finished in about 3 seconds, not 8 seconds. They ran concurrently.

What Makes Goroutines Special

Traditional threads are heavyweight. Each thread needs about 1MB of memory just to exist. Creating thousands means gigabytes of RAM.
Goroutines flip this on its head:
Go blog diagram 3

Go blog diagram 3

FeatureOS ThreadsGoroutines
Initial Stack Size~1MB~2KB
Creation TimeSlow (kernel call)Fast (user space)
Context SwitchExpensiveCheap
Typical LimitThousandsMillions
ManagementManualAutomatic
This means you can create 100,000 goroutines without breaking a sweat. Try that with threads and watch your server crash.

A Real World Example: Web Scraper

Let's build something practical. Imagine checking if multiple websites are online:
go
// Filename: website_checker.go package main import ( "fmt" "net/http" "time" ) // checkWebsite verifies if a website responds // Why: Real-world use case for concurrent operations // Parameters: // - url: website to check // Time Complexity: O(1) per request, but wait time varies func checkWebsite(url string) { start := time.Now() // Create HTTP client with timeout // Why: Prevents hanging forever on unresponsive sites client := http.Client{ Timeout: 5 * time.Second, } resp, err := client.Get(url) duration := time.Since(start) if err != nil { fmt.Printf("FAIL: %s (Error: %v)\n", url, err) return } defer resp.Body.Close() fmt.Printf("OK: %s (Status: %d, Time: %v)\n", url, resp.StatusCode, duration) } func main() { websites := []string{ "https://google.com", "https://github.com", "https://golang.org", "https://stackoverflow.com", "https://amazon.com", } start := time.Now() // Check all websites concurrently // Why: Network I/O is perfect for concurrency for _, site := range websites { go checkWebsite(site) } // Wait for completion // Note: We'll learn better synchronization methods later time.Sleep(6 * time.Second) fmt.Printf("\nTotal time: %v\n", time.Since(start)) }
Expected Output:
OK: https://google.com (Status: 200, Time: 245ms) OK: https://golang.org (Status: 200, Time: 312ms) OK: https://github.com (Status: 200, Time: 287ms) OK: https://stackoverflow.com (Status: 200, Time: 456ms) OK: https://amazon.com (Status: 200, Time: 523ms) Total time: 6.001s
Without goroutines, this would take the sum of all request times. With goroutines, all requests happen simultaneously.

The Execution Model Explained

Go blog diagram 4

Go blog diagram 4

When you write go someFunction(), here's what happens:
  1. Goroutine Creation: Go allocates a small stack (about 2KB)
  2. Scheduling: The Go scheduler adds it to a run queue
  3. Execution: The scheduler runs goroutines across available CPU cores
  4. Completion: When done, the goroutine's resources are cleaned up
You don't manage any of this. The Go runtime handles scheduling, growing stacks when needed, and multiplexing goroutines onto OS threads.

When Concurrency Shines

Concurrency works best when your program spends time waiting. This includes:
I/O Operations
  • Reading/writing files
  • Network requests
  • Database queries
Independent Tasks
  • Processing items in a batch
  • Handling separate user requests
  • Running parallel computations
Go blog diagram 5

Go blog diagram 5

Common Mistakes to Avoid

Mistake 1: Forgetting that main() exits immediately
go
// WRONG: Program exits before goroutine runs func main() { go fmt.Println("Hello!") // Program ends here, goroutine never prints } // RIGHT: Wait for goroutine to complete func main() { go fmt.Println("Hello!") time.Sleep(time.Second) // Gives goroutine time to run }
Mistake 2: Creating goroutines in a loop with shared variable
go
// WRONG: All goroutines might print the same value for i := 0; i < 5; i++ { go func() { fmt.Println(i) // Captures i by reference }() } // RIGHT: Pass i as a parameter for i := 0; i < 5; i++ { go func(n int) { fmt.Println(n) // Each goroutine gets its own copy }(i) }
Mistake 3: Using time.Sleep for synchronization in production
We used time.Sleep in examples for simplicity. Real applications use proper synchronization like WaitGroups or channels, which we'll cover in the next article.

Performance Comparison

Approach1000 RequestsMemoryComplexity
Sequential100 secondsLowSimple
Goroutines~1 secondModerateModerate
Thread per request~1 secondVery HighComplex
Goroutines give you the speed of parallel execution without the memory explosion of threads.

What You Learned

You now understand that:
  • Goroutines are lightweight: Create thousands without worry
  • The go keyword is simple: Just prefix any function call
  • Concurrency speeds up I/O bound tasks: Network, disk, and database operations benefit most
  • The Go runtime manages complexity: No manual thread management

Your Next Steps

  • Practice: Convert a sequential file processor to use goroutines
  • Read Next: Learn about channels for goroutine communication
  • Explore: Check out the runtime package to see goroutine statistics
The coffee shop now has multiple baristas. But they need to communicate without bumping into each other. That's where channels come in, and that's our next topic.

Quick Reference

go
// Create a goroutine go someFunction() // Anonymous goroutine go func() { // do something }() // With parameters go func(x int) { fmt.Println(x) }(42)
Concurrency in Go is not about making things complicated. It's about making things possible. With goroutines, you have the power to handle thousands of tasks efficiently. The syntax is simple. The results are powerful.
All Blogs
Tags:golangconcurrencygoroutinesparallel-programming