Handling Concurrency in Golang : Part 2

Dipto Chakrabarty
5 min readOct 4, 2024

--

Let us look at how channels can help synchronize concurrent operations in golang.

Firstly how do we run a function as a goroutine

RUNNING A FUNCTION AS GOROUTINE

In golang it is easy to run a function as goroutine , just by appending the go keyword a function runs as a goroutine.

go somefunc()

A simple example would be to run a hello world function as a goroutine

package main

import (
"fmt"
"time"
)

func HelloWorld() {
fmt.Println("Hello World from goroutine!")
}

func main() {
// Start the HelloWorld function as a goroutine
go HelloWorld()

// Allow some time for the goroutine to finish
time.Sleep(1 * time.Second)

fmt.Println("Main function is done.")
}

This runs the HelloWorld function as a goroutine and the main function as a separate go routine.

go run main.go
Hello World from goroutine!
Main function is done.

What are Channels and why do we need them.

Lets answer first why we need them , when we are using multiple go routines we want to make sure that the critical section of our code is being accessed and modified by a single go routine.

Furthermore we need a method in which we can pass data around the different go routines which are running concurrently.

Channels helps us to do this , the simplest explanation of it would be that it is like a buffer in which we can put data notify other go routines and then the data is removed.

Playing with channels

The sytax for declaring channels in go is very simple

someChan := make(chan int)

The chan keyword tells go that someChan is a channel of type bool.

For sending data to a channel we use the <- symbol

someChan <- 10

This sends the value 10 to the channel someChan. To get this value we can use the <- again but this time as if it is coming out of the channel.

value := <- someChan

This basically tells go to push out whatever it has in someChan and put it into the variable value.

A simple example of channels which shows sharing variables between go routines.

package main

import (
"fmt"
"time"
)

var someChan = make(chan int)

func producer() {
for i := 1; i <= 5; i++ {
time.Sleep(500 * time.Millisecond) // Simulate some work
fmt.Println("sending value ", i*10)
someChan <- i*10// Send a value to the channel
}
defer close(someChan) // Close the channel when done
}

func consumer() {
for {
msg, ok := <-someChan // Get value from the channel
if !ok {
// If the channel is closed, exit the loop
fmt.Println("Channel closed, exiting.")
return
}
fmt.Println("Received:", msg) // Print the received number
}
}

func main() {
go producer() // Start the producer goroutine
consumer() // Start the consumer in the main goroutine
}

Here there are three parts

someChan : a channel which can store int data types and will be used for sharing data between the producer go routine and main go routine

producer: a simple go routine which sends some integers to the channel

consumer: a simple function which when notices there is some data in the channel stores it in msg and prints it out.

An important point to note here is that the channel can hold only one value at a time and it has to be first removed before another value can be inserted.

So if 10 was never removed by the consumer function 20 would have never been able to enter someChan.

The select statement in golang

The select is a powerful method which we can synchronize and maintain logic when working with multiple channels.

Consider this similar to working with switch case except for channels.

The syntax for this is like this

select {
case s1 := <- s1chan:
..... some logic
case s2:= <- s2chan:
...... some other logic
}

This can help us model our logic based on the data we receive from which channel , if we are continuously sending data to s1chan the s1 block is executed.

So the select decides which block it will execute based on which channel has data.

Building on the previous example if we send even and odd numbers to two different channels and print different messages for each.


package main

import (
"fmt"
"time"
)

var evenChan = make(chan int)
var oddChan = make(chan int)
var closeChan = make(chan bool)

func producer() {
for i := 1; i <= 10; i++ {
time.Sleep(500 * time.Millisecond) // Simulate some work
fmt.Println("sending value ", i)
if i%2==0{
evenChan <- i// Send even value to the even channel
}else {
oddChan <- i
}
}
closeChan <- true
defer close(evenChan) // Close the channel when done
defer close(oddChan)
}

func consumer() {
for {
select {
case val := <- oddChan:
fmt.Println("\nThe odd value received is: ", val)
case val := <- evenChan:
fmt.Println("\nThe even value received is: ", val)
case <-closeChan:
return
}
}
}

func main() {
go producer() // Start the producer goroutine
consumer() // Start the consumer in the main goroutine
}

The output looks like this.

When value is received on the even channel that block of logic executes and when odd channel that block executes.

The closeChan is just to make sure we close the go routine which is done using the return keyword.

The real life use cases are far more complex compared to these.

If you liked this please consider liking subscribing , the next article will be on a bit more complex example.

--

--

Dipto Chakrabarty
Dipto Chakrabarty

Written by Dipto Chakrabarty

MS @CMU , Site Reliability Engineer , I talk about Devops Backend and AI. Tech Doctor making sure to diagnose and make your apps run smoothly in production.

No responses yet