Handling Golang Concurrency using Channels and Select : Part 1
One of the main advantages of golang is its powerful concurrency feature.
Let us explore concurrency in golang using select statements and understand how we can build a simple program.
What are go routines
A goroutine is a lightweight, concurrent function execution in Go. They are Go’s fundamental concurrency unit. They are similar to threads, but are managed by the Go runtime rather than the operating system.
Key features include:
- Lightweight: Goroutines use minimal memory and resources.
- Concurrent: Multiple goroutines can run simultaneously.
- Easy to create: Simply use the
go
keyword before a function call. - Independent execution: Goroutines run independently of the calling function.
How to run functions as go routines
Running a function as go routine is easy in golang , all we have to do is append the keyword go in front of a function for it to work as a goroutine.
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("This is hellow world func\n")
}
func main() {
go hello()
time.Sleep(time.Second)
fmt.Println("Main function finished")
}
This program spawns two goroutines one is the main goroutine which runs the hello function sleeps and prints the message and the other is a go routine to just run the hello function.
The output we get is this
However if we remove the time.Sleep our goroutine does not run
This is because our main routine executes and finishes so fast that we do not get the output of our hello function go routine.
Lets look at another problem , we are going to loop and print some values.
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go func() {
fmt.Printf("Goroutine %d is working\n", i)
}()
}
time.Sleep(time.Second)
fmt.Println("Main function finished")
}
The output of the following is this
There are two potential issues with our code
- The go routines other than main might take longer than main to execute and hence never end and deliver the results to us.
- The value of i is being shared in all goroutines.
To tackle such things we make use of channels.
What are channels and how to use them
Channels are like message buffers within go , consider them as something similar to a publisher subscriber in terms of operations.
The advantage of channels is that they allow us to share , update variables between go routines.
The best representation of channels I found was here (source Internet)
The operation is such that go routine 1 pushes some data to the channel. So the amount of data in the channel is 1.
Go routine 2 is continuously monitoring the channel.
Once data comes into the channel go routine 2 immediately picks up the value.
This empties the channel again so that goroutine 1 can pick it up again.
Well this solves the variables sharing problem right how about the go routines taking longer than the main routine problem.
Well the thing is that the channel can only hold 1 variable of data , to push other variables say from go routine 1 the channel must first be emptied by go routine 2.
We are going to achieve this thing using select statements in golang.
A simple code implementation
Run the following code to understand channels
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
// Producer goroutine
go func() {
for i := 1; i <= 5; i++ {
fmt.Printf("Pushing value : %d\n", i)
ch <- i
time.Sleep(time.Millisecond * 500)
}
defer close(ch)
}()
// Consumer (main goroutine)
for {
select {
case val, ok := <-ch:
if !ok {
fmt.Println("Channel closed, exiting")
return
}
fmt.Printf("Consumed: %d\n", val)
case <-time.After(time.Second):
fmt.Println("Timeout: no value received in 1 second")
}
}
}
The output produced is this
What are we doing
- We create channel called ch , the syntax for creating channels is this : make(chan {datatype})
- The producer go routine is running in the background and it is producing values 1 to 5.
- Every time it produces a value it sends it to the channel. The syntax for sending , receiving data in channels is <- and ->.
- Below you can see a loop is running infinitely which makes use of select.
- Consider select as a switch case for channels, what it basically says is if there is some data in channel ch we are going to make the channel spit it out and store that data in variable val.
- As pointed earlier the channel can hold one variable only at a time so until the value is emptied from the channel the producer go routine cannot push its next value into the channel and will have to wait.
- This does not completely solve the issue of main routine completing before the spawned go routines , that we will look into the next part.
This is how select is used for synchronization of data between go routines and demonstrates several key concepts:
- Goroutine communication: The producer and consumer communicate through a channel.
- Non-blocking operations: The
select
statement allows non-blocking channel operations. - Graceful shutdown: We handle the closing of the channel to exit the program cleanly.
- Timeout handling: The
select
statement includes a timeout case to avoid indefinite blocking.
The next upcoming blogs we will look into a bit more complex example where we try modifying a dictionary at the same time and how channels have to be synchronized to prevent it.