Concurrency in Go: Goroutines, Channels, and async/await (Comparison)

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

Why Did Go Outpace Everyone in Concurrency?

\n\n

Imagine a chef preparing ten dishes simultaneously. He doesn't race frantically from stove to stove — he simply switches efficiently between tasks: while the meat is stewing, he chops the salad; while the water is boiling, he plates the dish. In the programming world, this is called concurrency — the ability to manage multiple tasks without waiting for each one to finish.

\n\n

The Go language (Golang) was born precisely to solve concurrency problems. Its creators at Google grew tired of the complexities of multithreading in C++ and Java. They invented goroutines — lightweight threads that are launched with a single line of code. In this article, we will break down how concurrency works in Go, compare it with OS threads and the async/await paradigm from Python, and most importantly, show real code examples you can apply today.

\n\n

Goroutines vs OS Threads: Battle of the Light Cavalry

\n\n

When a developer hears "multithreading," they usually picture operating system threads. But in Go, it's different. Let's break down the key differences.

\n\n

What is an OS Thread?

\n

A thread is a heavy object managed by the OS kernel. Each thread has its own stack (usually 1-8 MB), requires system calls for creation and switching. Creating 10,000 threads would kill memory and CPU. Go is built on a different philosophy.

\n\n

Goroutine — Go's Lightweight Thread

\n

A goroutine is a function that executes concurrently. It is launched with the keyword go. Key features:

\n
    \n
  • Stack size: starts at 2-4 KB and grows/shrinks dynamically.
  • \n
  • Creation: thousands of goroutines are created in milliseconds.
  • \n
  • Scheduler: the Go runtime distributes goroutines across OS threads (usually as many as CPU cores).
  • \n
  • Syntax: simply add go before a function call.
  • \n
\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc sayHello(name string) {\n    for i := 0; i < 3; i++ {\n        fmt.Println("Hello,", name)\n        time.Sleep(100 * time.Millisecond)\n    }\n}\n\nfunc main() {\n    // Launch two goroutines\n    go sayHello("Go")\n    go sayHello("Goroutine")\n\n    // Give goroutines time to execute\n    time.Sleep(1 * time.Second)\n    fmt.Println("Main function finished")\n}
\n\n

Result: messages from the two goroutines will be interleaved. Go automatically switches between them without blocking each other. Try running this code — you will see "Hello, Go" and "Hello, Goroutine" printed alternately.

\n\n

Channels: The Main Synchronization Tool

\n\n

Goroutines are great, but how do they communicate? Go has no shared memory by default. Instead, it uses the principle: "Don't communicate by sharing memory; share memory by communicating." This is done through channels.

\n\n

A channel is a typed "pipeline" through which goroutines send and receive data. It is safe, simple, and elegant.

\n\n

Example: Parallel Number Processing

\n
package main\n\nimport "fmt"\n\nfunc squareWorker(numbers <-chan int, results chan<- int) {\n    for n := range numbers {\n        results <- n * n\n    }\n    close(results)\n}\n\nfunc main() {\n    numbers := make(chan int, 5)\n    results := make(chan int, 5)\n\n    // Launch the worker\n    go squareWorker(numbers, results)\n\n    // Send data\n    numbers <- 2\n    numbers <- 4\n    numbers <- 6\n    close(numbers)\n\n    // Read results\n    for res := range results {\n        fmt.Println("Square:", res)\n    }\n}
\n\n

In this example, we use buffered channels (the second argument is the buffer size). The worker goroutine reads from the numbers channel, squares the v

Blogs

Book Recommendations