Mastering Go Concurrency: From Basics to Advanced Patterns

The article walks readers through Go's concurrency model, explaining lightweight goroutines and channel communication, demonstrates common patterns such as worker pools and fan‑in/fan‑out with concrete code, highlights typical pitfalls like race conditions, deadlocks and memory leaks, and offers practical best‑practice recommendations for safe concurrent programming.

Golang Shines
Golang Shines
Golang Shines
Mastering Go Concurrency: From Basics to Advanced Patterns

Go’s concurrency model is built around lightweight goroutine s and channels, which together provide a simple yet powerful way to write concurrent programs.

Goroutine – a cheap thread managed by the Go runtime. Creating one costs only a few kilobytes of stack.

// Create a goroutine
func main() {
    go func() {
        fmt.Println("Hello from goroutine")
    }()
    fmt.Println("Hello from main")
    time.Sleep(time.Second) // wait for goroutine to finish
}

Channel – the communication mechanism between goroutines.

// Use a channel for communication
func main() {
    ch := make(chan string)
    go func() {
        ch <- "Hello from goroutine" // send
    }()
    msg := <-ch // receive
    fmt.Println(msg)
}

Common concurrency patterns :

Worker pool – a fixed number of workers consume jobs from a buffered channel and write results back.

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d processing job %d
", id, j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // send jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // collect results
    for a := 1; a <= numJobs; a++ {
        <-results
    }
}

Fan‑in / Fan‑out – multiple goroutines produce values that are merged into a single output channel.

func generate(done <-chan struct{}, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }()
    return out
}

func square(done <-chan struct{}, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-done:
                return
            }
        }
    }()
    return out
}

func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)
    output := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {
    done := make(chan struct{})
    defer close(done)
    in := generate(done, 1, 2, 3, 4, 5)
    c1 := square(done, in)
    c2 := square(done, in)
    for n := range merge(done, c1, c2) {
        fmt.Println(n)
    }
}

Typical pitfalls and their fixes:

Race condition – unsynchronized access to shared variables can produce nondeterministic results.

var counter int
var wg sync.WaitGroup

func increment() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        counter++ // race condition
    }
}

func main() {
    wg.Add(2)
    go increment()
    go increment()
    wg.Wait()
    fmt.Println(counter) // may not be 2000
}

Fix with a mutex:

var mu sync.Mutex

func incrementSafe() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()
        counter++
        mu.Unlock()
    }
}

Deadlock – sending on an unbuffered channel without a receiver blocks forever.

func main() {
    ch := make(chan int) // unbuffered
    ch <- 1               // blocks, no receiver
    fmt.Println(<-ch)    // never reached
}

Fix by using a buffered channel or receiving in another goroutine:

func main() {
    ch := make(chan int, 1) // buffered
    ch <- 1
    fmt.Println(<-ch)
}

Memory leak – goroutine waiting on a channel that is never closed keeps the program alive.

func main() {
    ch := make(chan int)
    go func() {
        for range ch { // never exits
            // process data
        }
    }()
    // forgot to close ch
}

Fix by using context for cancellation and explicitly closing the channel:

func main() {
    ch := make(chan int)
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case data, ok := <-ch:
                if !ok {
                    return
                }
                // process data
            }
        }
    }()
    // ... use channel ...
    cancel()
    close(ch)
}

Best‑practice checklist for Go concurrency:

Use context to control goroutine lifetimes.

Synchronize goroutines with sync.WaitGroup.

Protect shared mutable state with mutexes or atomic operations.

Prefer buffered channels to limit concurrency and avoid blocking.

Detect goroutine leaks with the race detector and careful testing.

Use select to handle multiple channel operations safely.

Prefer unbuffered channels for hand‑off semantics when appropriate.

Set the number of goroutines based on workload and system resources.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConcurrencyGogoroutinerace-conditionchannelworker-poolfan-in-fan-out
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.