Asynchronous Programming in Go: A Complete Guide for Beginners

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

Asynchronous Programming in Go: A Complete Guide for Beginners

Go (Golang) was created with the needs of modern development in mind, where performance and parallelism play a key role. Unlike many languages where asynchrony is implemented through callbacks or promises, Go offers built-in and elegant mechanisms: goroutines and channels. This guide will introduce you to the basics of asynchronous programming in Go and show you how to write efficient concurrent code.



What are goroutines and how are they different from threads?

A goroutine is a lightweight thread of execution managed by the Go runtime. Unlike OS system threads, goroutines consume much fewer resources: their stack starts at a few kilobytes and can dynamically grow and shrink. This allows you to run millions of goroutines simultaneously without significant memory load.

Starting a goroutine is very simple — just add the keyword go before a function call:

package main

import ( "fmt" "time")

func sayHello() { fmt.Println("Hello from goroutine!")}

func main() { go sayHello() // Start a goroutine time.Sleep(100 * time.Millisecond) // Wait for the goroutine to execute fmt.Println("Main function completed")}

It is important to note: if the main main() function finishes, all running goroutines will be forcibly stopped. For synchronization, we use special tools.



Synchronization with WaitGroup

The simplest way to wait for goroutines to finish is to use sync.WaitGroup. It works like a counter: we increment it when starting a goroutine and decrement it upon completion. The main function blocks until the counter reaches zero.

package main

import ( "fmt" "sync" "time")

func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrement the counter upon completion fmt.Printf("Worker %d started work", id) time.Sleep(time.Second) fmt.Printf("Worker %d finished work", id)}

func main() { var wg sync.WaitGroup

for i := 1; i <= 3; i++ { wg.Add(1) // Increment the counter before starting go worker(i, &wg) }

wg.Wait() // Wait for all goroutines to finish fmt.Println("All workers have finished work")}

Note: wg.Add(1) is called before starting the goroutine, and wg.Done() is called inside it (usually via defer). This ensures the counter is correctly decremented even in case of a panic.



Channels: Communication between goroutines

Channels (channels) are typed "pipes" through which goroutines can safely exchange data. Channels provide synchronization by default: sending to a channel blocks the goroutine until another goroutine reads the value, and vice versa.

Channels are created using the make function:

package main

import "fmt"

func main() { // Create an unbuffered channel for integers ch := make(chan int)

// Start a goroutine that sends data go func() { ch <- 42 // Send a value to the channel }()

// Read from the channel (blocks until a value arrives) value := <-ch fmt.Println("Received value:", value)}

Channels can be:

  • Unbuffered — synchronous, sending blocks until received.
  • Buffered — asynchronous, sending blocks only when the buffer is full.
// Creating a buffered channel with capacity 3bufferedCh := make(chan string, 3)bufferedCh <- "A"bufferedCh <- "B"bufferedCh <- "C" // Does not block until full


Blogs

Book Recommendations