- [ between the brackets ]
- Posts
- Goroutines: An Introduction to Go's Concurrency Primitives
Goroutines: An Introduction to Go's Concurrency Primitives
Why so many prefer Go over C
What are Goroutines?
Go was designed at Google to handle large scale computational problems, and concurrency is baked into its very core. When we say "concurrency," we're referring to the ability of a program to handle multiple tasks at the same time. That's where Goroutines come in. A Goroutine is a lightweight thread of execution managed by the Go runtime.
// Defining a simple function
func sayHello() {
fmt.Println("Hello, Go!")
}
// Running the function as a goroutine
go sayHello()
In this example, sayHello()
would be executed as a Goroutine - denoted by go
before the function call. However, when this code is run, there is no output because when the main function returns, all Goroutines are abruptly stopped. Let's fix that by asking it to wait for a second.
func sayHello() {
fmt.Println("Hello, Go!")
}
go sayHello()
// Add some sleep in the main function
time.Sleep(time.Second)
This time, you should see "Hello, Go!" printed out. But relying on time.Sleep
is not a good practice for synchronizing Goroutines. Instead, we use something called channels.
Channels are used to synchronize and exchange data between Goroutines. Let's look at a simple example where two Goroutines communicate through a channel:
func sayHello(ch chan string) {
ch <- "Hello, Go!" // Send a value into the channel
}
ch := make(chan string)
go sayHello(ch)
message := <-ch // Receive a value from the channel
fmt.Println(message) // "Hello, Go!"
Here, we create a channel using make
, pass it to our Goroutine, and then use it to communicate back to our main routine.
While we've explored the basics, Goroutines really shine when managing a larger number of tasks. Imagine we're running a web server where each incoming request is handled by a separate Goroutine, or we have a complex computational task that we can break down into smaller parts and run them concurrently.
Let's take a simple example of computing factorial of first N numbers concurrently using Goroutines:
func factorial(n int, ch chan int) {
f := 1
for i := 1; i <= n; i++ {
f *= i
}
ch <- f
}
func main() {
ch := make(chan int)
for i := 1; i <= 20; i++ {
go factorial(i, ch)
}
for i := 1; i <= 20; i++ {
fmt.Println(<-ch) // This will print the factorials, but not necessarily in order
}
}
In this program, we are spawning 20 Goroutines to calculate factorials of numbers from 1 to 20 concurrently. But, the output is not ordered as we might have expected. This is because Goroutines are not guaranteed to execute in the order they were started. This unpredictability is something to be aware of when designing your concurrent systems.
Goroutines and channels are fundamental to Go and enable a whole range of powerful concurrency patterns. They're like the power tools in your toolbox: when used with care and understanding, they can help you build impressive things.
[ Zach Coriarty ]