Go Concurrency Backend

Golang Concurrency: Goroutines, Channels & Mutexes

LW
Li Wei
Go Systems Engineer
Nov 05, 2025
18 min read

What You'll Learn

How Golang handles concurrent programming effortlessly using Goroutines, how to communicate safely using Channels, and when to use Mutexes for state management.

The Concurrency Problem

Modern servers have dozens of CPU cores. Languages like Python (with its GIL) and Node.js (single-threaded event loop) struggle to fully utilize multi-core systems out of the box. Go was built from the ground up to solve this using the CSP (Communicating Sequential Processes) model.

1. Goroutines (Lightweight Threads)

A goroutine is a function executing concurrently with other goroutines in the same address space. They are incredibly lightweight — you can easily spawn 100,000 goroutines on a standard laptop, whereas 100,000 OS threads would crash the system.

go — Launching a Goroutine
package main

import (
    "fmt"
    "time"
)

func fetchAPI(url string) {
    // Simulate network request
    time.Sleep(1 * time.Second)
    fmt.Println("Fetched:", url)
}

func main() {
    // Prefixing 'go' runs the function asynchronously!
    go fetchAPI("https://api.github.com")
    go fetchAPI("https://google.com")

    fmt.Println("Requests started...")
    
    // We must wait, otherwise main() exits before goroutines finish
    time.Sleep(2 * time.Second)
    fmt.Println("Done")
}

2. Channels (Safe Communication)

Go's concurrency philosophy is: "Do not communicate by sharing memory; instead, share memory by communicating."

Channels are typed conduits through which you can send and receive values between goroutines safely, without needing explicit locks.

go — Channels
package main

import "fmt"

func processTask(id int, resultChan chan string) {
    // Do work...
    result := fmt.Sprintf("Task %d completed", id)
    
    // SEND data into the channel
    resultChan <- result
}

func main() {
    // Create a channel of strings
    ch := make(chan string)

    // Launch 3 workers concurrently
    for i := 1; i <= 3; i++ {
        go processTask(i, ch)
    }

    // RECEIVE data from the channel (this blocks until data arrives!)
    for i := 1; i <= 3; i++ {
        fmt.Println(<-ch) 
    }
}

3. The Select Statement

What if you want to listen to multiple channels at the same time? Or implement a timeout? The select statement lets a goroutine wait on multiple communication operations.

go — Select & Timeouts
package main

import (
    "fmt"
    "time"
)

func main() {
    fastChan := make(chan string)
    slowChan := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        fastChan <- "Fast API response"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        slowChan <- "Slow DB query"
    }()

    // Wait for the first channel to respond, or timeout
    select {
    case msg := <-fastChan:
        fmt.Println("Received:", msg)
    case msg := <-slowChan:
        fmt.Println("Received:", msg)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout! Systems took too long.")
    }
}

4. WaitGroups (Waiting for multiple tasks)

Using time.Sleep() to wait for goroutines is a terrible practice. Instead, use a sync.WaitGroup to wait for a collection of goroutines to finish.

go — WaitGroup
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Decrement counter when function exits
    
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // Increment counter
        go worker(i, &wg)
    }

    wg.Wait() // Block until counter goes back to 0
    fmt.Println("All workers finished. Exiting main.")
}

When to use Mutexes instead of Channels?

While channels are great for passing data, sometimes you just need to update a shared map or counter. Using sync.Mutex is perfectly valid and often more performant for simple state protection.

var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

Keep Reading

D
DevOps

Docker Networking Demystified: Bridge, Host & Overlay

8 min read Read More
C
Cloud

AWS IAM Roles vs Users vs Policies

10 min read Read More
P
Programming

Understanding Python's GIL & Multiprocessing

14 min read Read More