Goroutines Project
In this lesson, you will build a real-world mini project using goroutines. This project demonstrates how concurrency improves performance and how goroutines are used in production Go applications.
Instead of learning theory alone, you will design, code, and analyze a concurrent system step by step.
Project Overview
We will build a Concurrent Task Processor that:
- Processes multiple tasks in parallel
- Uses goroutines for concurrency
- Uses channels to communicate results
- Simulates real processing time
Real-World Scenario
Imagine you have a backend service that needs to process multiple user requests at the same time:
- Calculating order totals
- Sending notifications
- Fetching data from APIs
Using goroutines allows these tasks to run concurrently instead of sequentially.
Project Structure
Create a new Go file:
goroutines_project.go
Step 1: Import Required Packages
package main
import (
"fmt"
"time"
)
Step 2: Create a Task Function
This function simulates processing a task. Each task takes a different amount of time.
func processTask(taskID int, results chan<- string) {
time.Sleep(time.Duration(taskID) * time.Second)
results <- fmt.Sprintf("Task %d completed", taskID)
}
Each task:
- Runs independently
- Sleeps to simulate work
- Sends its result through a channel
Step 3: Launch Goroutines
Now we launch multiple goroutines to process tasks concurrently.
func main() {
results := make(chan string)
for i := 1; i <= 5; i++ {
go processTask(i, results)
}
for i := 1; i <= 5; i++ {
fmt.Println(<-results)
}
}
How This Works
Let’s break it down:
- Five goroutines start almost instantly
- Each goroutine processes a task independently
- Results are collected through a channel
The total execution time equals the longest task, not the sum of all tasks.
Sequential vs Concurrent Execution
If this code ran sequentially, total time would be:
- 1 + 2 + 3 + 4 + 5 = 15 seconds
With goroutines, total time is approximately:
- 5 seconds
This is the power of concurrency.
Improving the Project with Buffered Channels
Buffered channels can prevent blocking.
results := make(chan string, 5)
This allows goroutines to send results without waiting.
Adding Logging for Better Visibility
func processTask(taskID int, results chan<- string) {
fmt.Println("Starting task", taskID)
time.Sleep(time.Duration(taskID) * time.Second)
fmt.Println("Finishing task", taskID)
results <- fmt.Sprintf("Task %d completed", taskID)
}
Common Mistakes to Avoid
- Forgetting to receive from channels
- Creating goroutines without synchronization
- Using unbuffered channels incorrectly
- Launching too many goroutines without limits
Practice Exercises
Exercise 1
Modify the project to process 10 tasks instead of 5.
Exercise 2
Add task IDs and execution timestamps to the output.
Exercise 3
Convert this project to use a worker pool pattern.
Key Takeaways
- Goroutines allow true concurrent execution
- Channels safely share data between goroutines
- Concurrency improves performance dramatically
- Real projects benefit most from goroutines
What’s Next?
In the next lesson, you will build a complete web application project in Go using everything learned so far.