Go: GoRoutines, Channels, and Mutexes with Practical Examples

kalyan dahakekalyan dahake
4 min read

Concurrency is one of Go's superpowers. Whether you’re building high-performance APIs or simply speeding up tasks, GoRoutines, Channels, and Mutexes are essential tools in your toolbox. This blog will guide you through these concepts with examples, leading to a project where we fetch APIs concurrently.


1. What Are GoRoutines?

Think of GoRoutines as lightweight threads that let you run multiple tasks at the same time. Unlike threads, they’re super efficient, allowing you to spin up thousands of them without consuming a lot of memory.

Example: Baking Cookies with GoRoutines

Imagine you're running a bakery, where different tasks like baking cookies, cupcakes, and bread can run concurrently:

package main

import (
    "fmt"
    "time"
)

func bakeCookies() {
    fmt.Println("Baking cookies...")
    time.Sleep(2 * time.Second) // Simulates work
    fmt.Println("Cookies are ready!")
}

func main() {
    go bakeCookies() // Start a helper to bake cookies
    fmt.Println("I'm busy with other tasks!")
    time.Sleep(3 * time.Second) // Wait to see all work complete
}

Output:

I'm busy with other tasks!
Baking cookies...
Cookies are ready!

With GoRoutines, tasks can run independently without blocking each other.


2. Channels: Talking Between GoRoutines

GoRoutines are powerful, but how do they communicate? Channels act like walkie-talkies, enabling them to send and receive messages safely.

Example: Sending a Message

package main

import (
    "fmt"
)

func sendOrder(ch chan string) {
    ch <- "Cookies are ready!" // Send a message through the channel
}

func main() {
    ch := make(chan string) // Create a channel
    go sendOrder(ch)        // Start GoRoutine to send a message

    msg := <-ch // Receive the message
    fmt.Println(msg)
}

Output:

Cookies are ready!

Channels ensure seamless communication between GoRoutines while maintaining thread safety.


3. Mutex: Managing Shared Resources

Concurrency can create conflicts when GoRoutines share resources. For example, two helpers trying to use the same oven simultaneously. Mutexes (mutual exclusion) solve this by allowing only one GoRoutine to access a resource at a time.

Example: Protecting a Counter

package main

import (
    "fmt"
    "sync"
)

var counter = 0 // Shared resource
var mutex = &sync.Mutex{}

func increment(wg *sync.WaitGroup) {
    mutex.Lock()   // Lock the resource
    counter++      // Critical section
    mutex.Unlock() // Unlock the resource
    wg.Done()      // Notify the WaitGroup that we're done
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1) // Add a worker to the WaitGroup
        go increment(&wg)
    }

    wg.Wait() // Wait for all GoRoutines to finish
    fmt.Println("Final Counter:", counter)
}

Output:

Final Counter: 5

The mutex.Lock() and mutex.Unlock() calls ensure only one GoRoutine updates the counter at a time.


4. Final Project: Concurrent API Calls

Now, let’s bring it all together! Here, we’ll fetch multiple API endpoints concurrently using GoRoutines, collect responses using Channels, and track successful requests using a Mutex.

Code: Concurrent API Calls

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

var successCount int          // Shared counter for successful API calls
var mutex = &sync.Mutex{}     // Mutex to protect the shared counter
var wg sync.WaitGroup         // WaitGroup to wait for all GoRoutines to finish

func fetchAPI(url string, ch chan string) {
    defer wg.Done() // Mark this GoRoutine as done
    client := &http.Client{Timeout: 5 * time.Second}

    resp, err := client.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("Failed to fetch %s: %v", url, err)
        return
    }
    defer resp.Body.Close()

    // If the response is successful, update the counter
    if resp.StatusCode == http.StatusOK {
        mutex.Lock()
        successCount++
        mutex.Unlock()
        ch <- fmt.Sprintf("Success: %s", url)
    } else {
        ch <- fmt.Sprintf("Failed: %s - Status Code: %d", url, resp.StatusCode)
    }
}

func main() {
    // List of API endpoints to fetch
    apiEndpoints := []string{
        "https://jsonplaceholder.typicode.com/posts/1",
        "https://jsonplaceholder.typicode.com/posts/2",
        "https://jsonplaceholder.typicode.com/posts/3",
        "https://jsonplaceholder.typicode.com/posts/4",
        "https://invalid-url.com", // Invalid URL to simulate an error
    }

    // Channel to collect responses
    responseChannel := make(chan string)

    // Start a GoRoutine for each API call
    for _, url := range apiEndpoints {
        wg.Add(1)
        go fetchAPI(url, responseChannel)
    }

    // GoRoutine to close the channel after all work is done
    go func() {
        wg.Wait()       // Wait for all GoRoutines to finish
        close(responseChannel) // Close the channel
    }()

    // Read responses from the channel
    fmt.Println("API Call Results:")
    for res := range responseChannel {
        fmt.Println(res)
    }

    // Print the final count of successful API calls
    fmt.Printf("\nNumber of successful API calls: %d\n", successCount)
}

What’s Happening Here?

  1. GoRoutines: Each API call runs concurrently in a separate GoRoutine.

  2. Channels: Collect responses (success or error) from each API call.

  3. Mutex: Ensures the successCount is updated safely when an API call succeeds.

  4. WaitGroup: Waits for all API calls to complete before closing the channel.

Output Example:

API Call Results:
Success: https://jsonplaceholder.typicode.com/posts/1
Success: https://jsonplaceholder.typicode.com/posts/2
Success: https://jsonplaceholder.typicode.com/posts/3
Success: https://jsonplaceholder.typicode.com/posts/4
Failed to fetch https://invalid-url.com: Get "https://invalid-url.com": dial tcp: lookup invalid-url.com: no such host

Number of successful API calls: 4

Conclusion

By combining GoRoutines, Channels, and Mutexes, we’ve built a robust concurrent system for fetching APIs. This structure can be extended to more complex systems, such as:

  • Real-time data processing

  • Parallel file uploads/downloads

  • Microservices orchestration

Mastering these concepts will empower you to build fast, efficient, and scalable applications in Go. Happy coding:) 🚀

0
Subscribe to my newsletter

Read articles from kalyan dahake directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

kalyan dahake
kalyan dahake

I'm building systems across industries, ensuring seamless software delivery. I manage everything from system design to deployment, driving operational excellence and client satisfaction.