In Go, we can make our program do more than one thing at the same time. This is called concurrency, and it’s a great way to get things done faster.
we'll walk through goroutines, channels, and WaitGroups with simple examples you can try yourself.
What is concurrency?
imagine you’re making dinner. while the pasta is boiling, you start cutting vegetables for the salad. you’re doing both at once so the food is ready sooner.
that’s how concurrency works in a program. you run different tasks side by side instead of waiting for each one to finish before starting the next.
Goroutines: Your Lightweight Workers
A goroutine is like hiring an extra worker for your program. it's incredibly cheap to create (we can have thousands of them) and they all work together.
simple example: without Goroutines
func main() {
cookPasta() // takes 10 minutes
makeSalad() // takes 5 minutes
bakeBread() // takes 15 minutes
// Total time: 30 minutes
}
with Goroutines
func main() {
go cookPasta() // start cooking pasta in background
go makeSalad() // start making salad in background
go bakeBread() // start baking bread in background
time.Sleep(16 * time.Minute) // Wait for everything to finish
// Total time: 16 minutes (everything runs together!)
}
the go keyword is like saying "hey, start doing this task in the background while I continue with other things."
The Problem: How Do We Know When Work is Done?
here's the issue with the restaurant example above. how do we know when all the chefs are done? we used time.Sleep() which is like guessing how long it will take. that's not reliable.
Solution 1: WaitGroups (The Team Counter)
A WaitGroup is like a counter that keeps track of how many workers are still busy.
func main() {
var wg sync.WaitGroup // create a counter
wg.Add(1) // "I'm starting 1 worker"
go func() {
defer wg.Done() // "this worker is finished"
cookPasta()
}()
wg.Add(1) // "I'm starting another worker"
go func() {
defer wg.Done() // "this worker is finished"
makeSalad()
}()
wg.Wait() // "wait until all workers are done"
fmt.Println("all food is ready")
}
When to use WaitGroups:
- you have a fixed number of tasks
- you just want to wait for everything to finish
- you don't need results back from the workers
Solution 2: Channels (The Communication System)
Channels are like walkie-talkies that let your goroutines talk to each other. they can send messages, data, or even just signals saying "I'm done"
basic Channel example
func main() {
messages := make(chan string) // create a walkie-talkie
// Worker sends a message
go func() {
time.Sleep(2 * time.Second)
messages <- "Pizza is ready!" // send message through channel
}()
// main goroutine receives the message
msg := <-messages // wait for and receive message
fmt.Println(msg) // pints: "Pizza is ready!"
}
Using Channels for Completion Signals
func cookWithChannels() {
done := make(chan bool) // create a "done" signal channel
go func() {
cookPasta()
done <- true // send "finished" signal
}()
go func() {
makeSalad()
done <- true // send "finished" signal
}()
// wait for both tasks to finish
<-done // wait for first task
<-done // wait for second task
fmt.Println("both tasks completed!")
}
Real World Example: Processing Multiple Files
let's say you want to process 100 files. here's how you'd do it:
Without Concurrency (Slow)
func processFiles(files []string) {
for _, file := range files {
processFile(file) // process one file at a time
}
// If each file takes 1 second, 100 files = 100 seconds
}
With Goroutines and WaitGroups (Fast)
func processFilesConcurrently(files []string) {
var wg sync.WaitGroup
for _, file := range files {
wg.Add(1)
go func(fileName string) {
defer wg.Done()
processFile(fileName)
}(file)
}
wg.Wait() // wait for all files to be processed
// with 10 CPU cores, 100 files might take only 10 seconds
}
With Goroutines and Channels (Getting Results Back)
func processFilesWithResults(files []string) {
results := make(chan string, len(files)) // buffered channel
// start workers
for _, file := range files {
go func(fileName string) {
result := processFile(fileName)
results <- result // send result back
}(file)
}
// Collect results
for i := 0; i < len(files); i++ {
result := <-results
fmt.Println("Got result:", result)
}
}
When to Use What?
use Goroutines when:
- you want to do multiple things at the same time
- tasks are independent of each other
- you want to improve performance
use WaitGroups when:
- you have a known number of workers
- you just need to wait for everything to finish
- you don't need results or communication
use Channels when:
- workers need to send data back
- you need coordination between goroutines
- you want to control the flow of work
Simple Worker Pool Pattern
func workerPool(jobs []string, numWorkers int) {
jobChan := make(chan string)
// start workers
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobChan { // keep taking jobs until channel closes
fmt.Printf("Worker %d processing %s\n", workerID, job)
// do actual work here
time.Sleep(1 * time.Second)
}
}(i)
}
// send jobs
go func() {
for _, job := range jobs {
jobChan <- job
}
close(jobChan) // signal no more jobs
}()
wg.Wait() // wait for all workers to finish
fmt.Println("all jobs completed!")
}
- Goroutines are cheap and easy.
- WaitGroups are perfect for "fire and forget" scenarios
- Channels are great when you need communication or results
Top comments (0)