Concurrency and Multithreading in Go: Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Concurrency and Multithreading in Go: Advanced Level



Go (Golang) was designed from the start as a language for writing scalable and performant applications. At its core lies a powerful concurrency model based on goroutines and channels. If you've already mastered basic examples with go func() and simple channels, it's time to move to the next level. In this article, we will explore advanced concurrency patterns in Go that will help you write reliable, efficient, and safe code.



1. Managing Goroutine Lifecycle: Context and WaitGroup



One of the most common problems is goroutine leaks. A goroutine that endlessly waits for data from a channel will never complete if the channel is closed incorrectly. Go provides two main tools for managing goroutine lifecycles: sync.WaitGroup and context.Context.



sync.WaitGroup for Waiting for Completion

Use sync.WaitGroup when you need to wait for a fixed set of goroutines to finish. It's a simple and efficient mechanism.



package main

import (\t"fmt"\t"sync"\t"time")

func worker(id int, wg *sync.WaitGroup) {\tdefer wg.Done() // Decrement the counter upon exit\tfmt.Printf("Worker %d started work", id)\ttime.Sleep(time.Second)\tfmt.Printf("Worker %d finished work", id)}

func main() {\tvar wg sync.WaitGroup

\tfor i := 1; i <= 3; i++ {\t\twg.Add(1) // Increment the counter before launching\t\tgo worker(i, &wg)\t}

\twg.Wait() // Block until the counter becomes 0\tfmt.Println("All workers have finished")}


context.Context for Cancellation and Deadlines

When a goroutine runs in a loop or performs a long operation, it needs to be able to terminate gracefully upon an external signal. context.Context is the standard way to pass a cancellation signal, deadline, or timeout.



package main

import (\t"context"\t"fmt"\t"time")

func longRunningTask(ctx context.Context) {\tfor {\t\tselect {\t\tcase <-ctx.Done():\t\t\tfmt.Println("Goroutine received stop signal")\t\t\treturn\t\tdefault:\t\t\ttime.Sleep(500 * time.Millisecond)\t\t\tfmt.Println("Working...")\t\t}\t}}

func main() {\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\tdefer cancel() // Important to call cancel to free resources

\tgo longRunningTask(ctx)

\ttime.Sleep(3 * time.Second)\tfmt.Println("Main finished")}


2. Channels: Buffering, Closing, and the Fan-Out/Fan-In Pattern



Channels are the primary way to synchronize and pass data between goroutines. At an advanced level, it's important to understand the difference between buffered and unbuffered channels, close them correctly, and use aggregation patterns.



Buffered vs Unbuffered

An unbuffered channel (no buffer) blocks the sender until the receiver reads the data. A buffered channel allows sending several messages without waiting until the buffer is full. The choice depends on the task: use unbuffered for strict synchronization, and buffered for smoothing load spikes.



Fan-Out/Fan-In Pattern

This is one of the most powerful patterns. Fan-Out distributes tasks across multiple goroutines (workers). Fan-In collects results from multiple goroutines into a single channel.



package main

import (\t"fmt"\t"sync")

func producer(nums ...int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor _, n := range nums {\t\t\tout <- n\t\t}\t\tclose(out)\t}()\treturn out}

func square(in <-chan int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor n := range in {\t\t\tout <- n * n\t\t}\t\tclose(out)\t}()\treturn out}

func merge(channels ...<-chan int) <-chan int {\t

Blogs

Book Recommendations