Golang - Concurrency - Goroutines and Channels Fundamentals
Go was designed from the ground up for concurrency, and its approach is a major reason for its popularity. The model is built on two simple but powerful concepts: goroutines and channels.
The core philosophy is: “Don’t communicate by sharing memory; instead, share memory by communicating.”
1. Goroutines: Lightweight Threads
A goroutine is an independently executing function that runs concurrently with other code. You can think of it as a very lightweight thread, but it’s managed by the Go runtime, not the OS.
Analogy: Imagine you are a chef in a kitchen.
- Traditional Threads: You hire another chef (an OS thread). This is expensive, they take up a lot of space, and coordinating with them is slow.
- Goroutines: You snap your fingers, and a magical kitchen helper appears (a goroutine). They are incredibly cheap, take up almost no space, and you can have thousands of them working at once without chaos.
Key Features of Goroutines:
- Lightweight: A goroutine starts with a tiny stack (around 2KB), which is much smaller than the 1MB+ stack of a typical OS thread. This is why you can have millions of goroutines in a single program.
- Fast Startup: Creating a goroutine is much faster than creating an OS thread.
- Go Runtime Scheduler: The Go runtime has its own scheduler that multiplexes goroutines onto a small number of OS threads. It can intelligently manage them, for example, by moving a goroutine off a thread if it’s blocked on I/O.
How to Start a Goroutine
It’s incredibly simple: just use the go
keyword before a function call.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start a new goroutine that executes the say() function.
go say("world")
// The main function continues to execute in its own goroutine.
say("hello")
}
Output (order may vary):
hello
world
hello
world
hello
world
The program doesn’t wait for the go say("world")
goroutine to finish. The main
function’s goroutine and the new goroutine run concurrently.
2. Channels: The Communication Pipes
Channels are the primary way for goroutines to communicate and synchronize with each other. A channel is a typed conduit, like a pipe, that you can send values into and receive values from.
Analogy: A channel is like a conveyor belt between two kitchen helpers.
- One helper can put an item on the belt (send to the channel).
- Another helper can take an item off the belt (receive from the channel).
- The belt ensures that items are delivered safely and in order.
Key Features of Channels:
- Typed: A channel can only transport values of a specific type (e.g.,
chan int
,chan string
). - Synchronization: By default, channels are unbuffered. This means a send operation will block until a receive operation is ready, and vice-versa. This blocking is the key to synchronization. It’s a “rendezvous” point for two goroutines.
How to Use Channels
package main
import "fmt"
// This function calculates the sum and sends the result to a channel.
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // Send sum to channel c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
// Create a channel to communicate the result.
c := make(chan int)
// Start two goroutines to sum the two halves of the slice.
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
// Receive the results from the channel.
// This will block until both goroutines have sent their results.
x, y := <-c, <-c
fmt.Println(x, y, x+y) // -1 12 11
}
In this example, the main
goroutine starts two sum
goroutines. It then waits to receive two values from the channel c
. It doesn’t matter which goroutine finishes first; main
will block until both results are available, demonstrating the power of channels for both communication and synchronization.