Introduction to Concurrency in Go
The Go language (Golang) was developed at Google with modern requirements for performance and scalability in mind. One of the key features of Go is built-in support for concurrency — the ability of a program to execute multiple tasks simultaneously. Unlike traditional multithreading, where threads are managed by the operating system, Go offers lightweight goroutines and convenient synchronization primitives — channels.
In this article, we will cover:
- What goroutines are and how to launch them
- Channels for exchanging data between goroutines
- Mutexes for safe access to shared resources
- Practical examples and patterns of concurrent code
1. Goroutines: Lightweight Threads of Go
A goroutine is a function that executes concurrently with other goroutines. It takes only a few kilobytes of stack space (unlike OS threads, where the stack can be megabytes). You can launch a goroutine simply by adding the keyword go before a function call.
Example: Launching goroutines
package main
import ( "fmt" "time")
func sayHello(name string) { for i := 0; i < 3; i++ { fmt.Printf("Hello, %s! (iteration %d)", name, i) time.Sleep(100 * time.Millisecond) }}
func main() { go sayHello("Alice") // Launching a goroutine go sayHello("Bob") // Another goroutine time.Sleep(1 * time.Second) // Wait for completion fmt.Println("Main function finished")}Important: The program terminates when the main function finishes, even if other goroutines are still running. Therefore, we use time.Sleep to wait. In real code, it's better to use sync.WaitGroup.
Using sync.WaitGroup
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrease the counter upon completion fmt.Printf("Worker %d started working", id) // ... some work ... fmt.Printf("Worker %d finished working", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Increase the counter before launching a goroutine go worker(i, &wg) }
wg.Wait() // Block main until all goroutines finish fmt.Println("All workers have finished their work")}2. Channels: Exchanging Data Between Goroutines
Channels are typed "pipes" through which goroutines can send and receive values. Channels provide safe synchronization without explicit locks.
Creating and Using Channels
package main
import "fmt"
func main() { // Create a channel for integers ch := make(chan int)
// Launch a goroutine that sends data go func() { ch <- 42 // Send a value to the channel }()
// Receive a value from the channel value := <-ch fmt.Println("Received:", value)}By default, channels are unbuffered: sending blocks until the receiver is ready to accept the data, and vice versa. This ensures synchronization.
Buffered Channels
package main
import "fmt"
func main() { // Channel with a buffer of 3 elements ch := make(chan string, 3)
// Send without blocking (until the buffer is full) ch <- "one" ch <- "two" ch <- "three"
// Read fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)}Closing Channels and range
package main
import "fmt"
func fibonacci(n int, ch chan int) { a, b := 0, 1 for i := 0; i < n; i++ { ch <-