Asynchronous Programming in Go: A Complete Guide with Practice

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: From Theory to Practice



Go (Golang) is famous for its built-in support for asynchronous programming. Unlike many other languages where asynchrony requires complex libraries and callbacks, Go offers elegant and powerful primitives: goroutines and channels. In this guide, we will break down all the key concepts in practice so you can write efficient, concurrent code in Go.



What are Goroutines?



A goroutine is a lightweight thread of execution managed by the Go runtime. Unlike system threads, goroutines consume very few resources (just a few kilobytes of stack), allowing you to run thousands or even millions of goroutines simultaneously.



Starting a goroutine is as simple as adding 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(time.Millisecond * 100) // Give the goroutine time to execute fmt.Println("Main function finished")}


Note: if you don't add time.Sleep, the program may finish before the goroutine starts executing. For synchronization, it's better to use other mechanisms, which we will cover below.



Channels: Passing Data Between Goroutines



Channels are typed "pipes" through which goroutines can exchange data. A channel is created using the make function and the chan operator. Data is sent to a channel using the <- operator, and received from it also using <-, but on the left side of the channel.



Let's look at an example with an unbuffered and a buffered channel:



package main

import "fmt"

func main() { // Unbuffered channel (size 0) ch := make(chan string) go func() { ch <- "Message from goroutine" // Send data }() msg := <-ch // Receive data (blocks until a message arrives) fmt.Println(msg) // Buffered channel (size 2) bufferedCh := make(chan int, 2) bufferedCh <- 10 bufferedCh <- 20 fmt.Println(<-bufferedCh) // 10 fmt.Println(<-bufferedCh) // 20}


Important: An unbuffered channel blocks the sender until the receiver accepts the data. A buffered channel blocks the sender only when the buffer is full.



Select: Multiplexing Channels



The select statement allows a goroutine to wait on multiple channels and handle the one that is ready first. It's analogous to switch, but for channels. select is a powerful tool for creating timeouts, canceling operations, and non-blocking checks.



package main

import ( "fmt" "time")

func main() { ch1 := make(chan string) ch2 := make(chan string) // Goroutine 1: sends data after 1 second go func() { time.Sleep(1 * time.Second) ch1 <- "Data from channel 1" }() // Goroutine 2: sends data after 2 seconds go func() { time.Sleep(2 * time.Second) ch2 <- "Data from channel 2" }() select { case msg1 := <-ch1: fmt.Println("Received:", msg1) case msg2 := <-ch2: fmt.Println("Received:", msg2) case <-time.After(500 * time.Millisecond): fmt.Println("Timeout! No channel responded within 500 ms") }}


In this example, select will trigger on the timeout, since neither channel is ready within 500 ms. If we removed the timeout, the program would wait for the first ready channel

Blogs

Book Recommendations