Concurrency and Multithreading in Go: Goroutines and Channels

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

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 <-

Blogs

Book Recommendations