What Are Goroutines?
Goroutines are Go's way of running multiple functions concurrently (at the same time). Think of them as extremely lightweight threads that let your program do multiple things at once without the complexity and resource overhead of traditional threading.
Real-World Analogy
Imagine a restaurant:
- Single-threaded program: One chef doing everything (taking orders, cooking, serving, cleaning)
- Traditional threads: Multiple chefs, each requiring their own kitchen space and tools
- Goroutines: Many assistant chefs sharing the same kitchen efficiently, coordinated by a head chef (the Go runtime)
How Goroutines Work Under the Hood
Goroutines are built on a concept called "multiplexing":
- Tiny Memory Footprint: A goroutine starts with just 2KB of stack memory (compared to megabytes for OS threads)
- Managed by Go's Runtime: Go uses an M:N scheduling model where:
- M = OS threads (usually matching CPU cores)
- N = Your goroutines (can be thousands or millions)
- Non-blocking: When a goroutine would block (like waiting for I/O), the Go scheduler automatically switches to another goroutine
This means you can create thousands of goroutines without exhausting system resources – something impossible with traditional threads.
Your First Goroutine
Creating a goroutine is as simple as adding the go
keyword before a function call:
package main
import (
"fmt" "time" )
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
// Run in background go sayHello()
// Main continues immediately fmt.Println("Main function is running")
// Give goroutine time to complete time.Sleep(1 * time.Second)
fmt.Println("Main function finished")
}
What's Happening Here?
- The
go
keyword startssayHello()
in a separate goroutine - The main function continues executing immediately (doesn't wait)
- We use
time.Sleep()
to prevent the program from ending before the goroutine completes
Output:
Main function is running
Hello from goroutine!
Main function finished
Note for Beginners: Without the
time.Sleep()
, the program might exit before the goroutine has a chance to run! This is a common mistake.
When to Use Goroutines
Goroutines shine when your program needs to:
- Handle multiple tasks simultaneously (like serving web requests)
- Keep the UI responsive while processing data
- Perform I/O operations (reading files, network requests) without blocking
- Take advantage of multiple CPU cores for computationally intensive tasks
Real-World Example: Faster Web Scraping
Let's compare sequential vs. concurrent approaches to fetching multiple websites:
package main
import (
"fmt" "io" "net/http" "strings" "sync" "time" )
func fetchWebsite(url string) (int, error) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
elapsed := time.Since(start)
fmt.Printf("Fetched %s in %v: %d bytes\n", url, elapsed, len(body))
return len(body), nil }
func main() {
websites := []string{
"https://golang.org",
"https://google.com",
"https://github.com",
"https://stackoverflow.com",
}
fmt.Println("=== Sequential approach ===")
sequentialStart := time.Now()
totalBytes := 0
for _, url := range websites {
bytes, err := fetchWebsite(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
continue }
totalBytes += bytes
}
fmt.Printf("Sequential approach took %v for %d total bytes\n\n",
time.Since(sequentialStart), totalBytes)
fmt.Println("=== Concurrent approach with goroutines ===")
concurrentStart := time.Now()
var wg sync.WaitGroup
totalBytesChannel := make(chan int)
errorChannel := make(chan string)
// Start a goroutine to sum up bytes go func() {
concurrentTotalBytes := 0 for bytes := range totalBytesChannel {
concurrentTotalBytes += bytes
}
fmt.Printf("Concurrent approach took %v for %d total bytes\n",
time.Since(concurrentStart), concurrentTotalBytes)
}()
// Start a goroutine to collect errors go func() {
for err := range errorChannel {
fmt.Println(err)
}
}()
// Launch a goroutine for each website for _, url := range websites {
wg.Add(1)
go func(url string) {
defer wg.Done()
bytes, err := fetchWebsite(url)
if err != nil {
errorChannel <- fmt.Sprintf("Error fetching %s: %v", url, err)
return }
totalBytesChannel <- bytes
}(url)
}
// Wait for all fetches to complete wg.Wait()
close(totalBytesChannel)
close(errorChannel)
// Allow time for the summary goroutines to finish time.Sleep(100 * time.Millisecond)
}
The concurrent version is typically 3-4x faster because it fetches all websites simultaneously instead of one after another.
Visualizing Goroutines vs Sequential Execution
Sequential Execution
Time →
Task 1 [===========] Task 2 [===========] Task 3 [===========] Total time: Sum of all tasks
Concurrent Execution with Goroutines
Time →
Task 1 [===========] Task 2 [===========] Task 3 [===========] Total time: Approximately the longest task
Common Pitfalls for Beginners
1. Forgetting Main Function Doesn't Wait
The program below will likely print nothing because the main function exits before the goroutine has a chance to run:
package main
import "fmt"
func printMessage() {
fmt.Println("This might never be seen!")
}
func main() {
go printMessage()
// Program exits immediately! }
Solution: Use synchronization mechanisms like WaitGroup
:
package main
import (
"fmt" "sync" )
func printMessage(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("This will definitely be seen!")
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go printMessage(&wg)
wg.Wait() // Wait for goroutine to finish }
2. Closure Variables in Loops
This is a classic mistake:
func main() {
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Most likely prints "5" five times! }()
}
time.Sleep(time.Second)
}
Solution: Pass variables as parameters or create local copies:
func main() {
for i := 0; i < 5; i++ {
go func(num int) {
fmt.Println(num) // Prints 0, 1, 2, 3, 4 (in some order) }(i)
}
time.Sleep(time.Second)
}
3. Race Conditions with Shared Data
This code has a race condition:
var counter int
func main() {
for i := 0; i < 1000; i++ {
go func() {
counter++ // DANGER: Concurrent writes! }()
}
time.Sleep(time.Second)
fmt.Println(counter) // Unpredictable result! }
Solution: Use mutexes or channels for synchronization:
var counter int var mu sync.Mutex
func main() {
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // Reliably shows 1000 }
Communication Between Goroutines: Introducing Channels
While goroutines run code concurrently, they often need to communicate results or coordinate actions. Go provides channels for this purpose:
func main() {
// Create a channel for integers resultsChannel := make(chan int)
// Start a goroutine that sends a value go func() {
time.Sleep(2 * time.Second) // Simulate work resultsChannel <- 42 // Send value to channel }()
fmt.Println("Waiting for result...")
result := <-resultsChannel // Receive from channel (blocks until data arrives) fmt.Println("Got result:", result)
}
We'll explore channels in depth in tomorrow's article.
Best Practices
- Start Simple: Don't add concurrency prematurely - it adds complexity
- Use WaitGroups: To properly wait for goroutines to complete
- Avoid Shared State: Use channels for communication when possible
- Handle Errors: Propagate errors from goroutines back to the main function
- Limit Goroutines: Consider using worker pools for resource-intensive tasks
Conclusion
Goroutines are the foundation of Go's concurrency model - incredibly lightweight threads that make concurrent programming accessible. They allow you to write code that efficiently utilizes your hardware while maintaining readability.
In tomorrow's article, we'll dive deeper into channels - Go's mechanism for communication between goroutines.
Exercises for Practice
- Modify the "Hello from goroutine" example to print messages from 5 different goroutines
- Write a program that calculates the sum of numbers from 1 to 1,000,000 using multiple goroutines
- Create a simple parallel file processing program that counts words in multiple text files concurrently
- Implement a concurrent version of a prime number finder
Tags: Golang, concurrency, goroutines, tutorial, Go programming, performance optimization, parallelism