DEV Community

Yash-007
Yash-007

Posted on

Go Concurrency: Goroutines, Channels and WaitGroups

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
}
Enter fullscreen mode Exit fullscreen mode

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!)
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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!"
}
Enter fullscreen mode Exit fullscreen mode

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!")
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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!")
}
Enter fullscreen mode Exit fullscreen mode
  • 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)