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

\n\n

Concurrency is one of the key features of the Go language, built into its architecture at the runtime and syntax level. Unlike many other languages where multithreading is implemented through complex libraries and requires manual thread management, Go offers lightweight goroutines and channels. At an advanced level, it is important not just to launch several tasks, but to synchronize them properly, avoid data races, and use system resources efficiently. In this article, we will cover advanced techniques for working with concurrency in Go.

\n\n

1. Goroutines: Lifecycle Management and Priorities

\n\n

Goroutines are lightweight threads managed by the Go runtime. They are launched using the go keyword. However, at an advanced level, you need to understand how to control their execution and completion.

\n\n

Basic goroutine launch:

\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc worker(id int) {\n    fmt.Printf("Worker %d starting\\n", id)\n    time.Sleep(time.Second)\n    fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n    for i := 1; i <= 5; i++ {\n        go worker(i)\n    }\n    time.Sleep(2 * time.Second) // Workaround: waiting for completion\n}
\n\n

Problem: in this example, we use time.Sleep for waiting, which is unreliable. At an advanced level, channels and sync.WaitGroup are used.

\n\n

2. Synchronization with sync.WaitGroup

\n\n

sync.WaitGroup allows you to wait for a specified number of goroutines to complete. This is a basic but powerful synchronization tool.

\n\n
package main\n\nimport (\n    "fmt"\n    "sync"\n    "time"\n)\n\nfunc worker(id int, wg *sync.WaitGroup) {\n    defer wg.Done() // Decrement the counter upon completion\n    fmt.Printf("Worker %d starting\\n", id)\n    time.Sleep(time.Second)\n    fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n    var wg sync.WaitGroup\n    for i := 1; i <= 5; i++ {\n        wg.Add(1) // Increment the counter before launching\n        go worker(i, &wg)\n    }\n    wg.Wait() // Block until the counter reaches zero\n    fmt.Println("All workers completed")\n}
\n\n

Important: wg.Add() must be called before launching the goroutine, otherwise a data race may occur. Use defer wg.Done() inside the goroutine to guarantee the counter is decremented even in case of a panic.

\n\n

3. Channels: Buffering, Direction, and select

\n\n

Channels are the primary mechanism for exchanging data between goroutines. At an advanced level, buffered channels, unidirectional channels, and the select statement are used.

\n\n

3.1. Buffered Channels

\n

Buffered channels allow sending data without immediate reception. This reduces blocking and increases throughput.

\n
package main\n\nimport "fmt"\n\nfunc main() {\n    ch := make(chan int, 3) // channel with a buffer of 3 elements\n    ch <- 1\n    ch <- 2\n    ch <- 3\n    // ch <- 4 // blocking, as the buffer is full\n    fmt.Println(<-ch)\n    fmt.Println(<-ch)\n    fmt.Println(<-ch)\n}
\n\n

3.2. Unidirectional Channels

\n

For type safety and explicit channel role specification, unidirectional channels are used: chan<- (send only) and <-chan (receive only).

\n
package main\n\nimport "fmt"\n\nfunc sender(out chan<- int) {\n    for i := 0; i < 5; i++ {\n        out <- i\n    }\n    close(out)\n}\n\nfunc receiver(in <-chan int) {\n    for val := range in {\n        fmt.Println(val)\n    }\n}\n\nfunc main() {\n    ch := make(chan int)\n    go sender(ch)\n    receiver(ch)\n}
\n\n

3.3. The select Statement

\n

select allows a goroutine to wait on multiple channels simultaneously. This is

Blogs

Book Recommendations