When we search term concurrency in Golang, we come around mainly to the goroutines
and channels
. Sometimes it's waitgroup
or mutex
. There are many terms that can explain concurrency in Golang. But we get confused with all things when we start implementing the same.
The same thing happened to me and as an outcome came up with this blog.
Today we will discuss concurrency in Golang with an example of sentiment data processing using goroutines, channels, waitgroups, and many more terms.
Sentiment data processing: It is a data processing example, in which we can analyze sentiment data from social media posts, comments, or any expression-related data, whether the data is positive, neutral, or negative.
Good concurrent habits take the ground in improving efficiency in life and work. Justly will aid your goals. Try it out today.
Concurrency in Go is one of the language’s key features and has been designed to make it easy to write highly concurrent and scalable applications.
It is the ability to execute multiple tasks or processes simultaneously, that allows efficient use of system resources and improves application performance.
Note that concurrency and parallelism are two different terms.
multiple tasks
at the same time
. It is a term used for writing programs.multiple tasks
with multiple resources
at the same time
. It is hardware related term, used when building CPUs and cores.Example — Students have been assigned a task to write a paragraph from the board.
Let’s start understanding all the terms step by step with the sentiment data processing
example.
Goroutines and channels are mainly used for achieving concurrency in Golang.
Goroutines can be thought of as functions that run independently in the background, allowing the program to perform multiple tasks simultaneously.
A goroutine is created using the go
keyword followed by a function call. The function is then executed concurrently in its own goroutine, allowing other parts of the program to continue running without waiting for the function to complete.
Channels are an essential part of concurrency in Go, allowing Goroutines to communicate with each other and share data.
A channel is a medium through which you can send and receive values with the channel operator, <-
. This operator can either send or receive data, depending on the side of the channel on which it is used.
package main
import (
"fmt"
"strings"
)
func analyzeSentiment(data string, resultChan chan string) {
// Perform sentiment analysis on the input data
// Here, we simply check if the input contains the word "happy"
if strings.Contains(strings.ToLower(data), "happy") {
resultChan <- "positive"
} else {
resultChan <- "negative"
}
}
func main() {
// Define the input data
input := []string{
"I am so happy today!",
"I hate this weather.",
"Happy birthday!!",
}
// Create a channel for the sentiment analysis results
resultChan := make(chan string)
// Launch a goroutine to analyze the sentiment of each input string
for _, data := range input {
go analyzeSentiment(data, resultChan)
}
// Wait for the results to be processed and print them
for i := 0; i < len(input); i++ {
fmt.Println(<-resultChan)
}
}
In the above code,
resultChan
to store the results of sentiment analysis.analyzeSentiment
is a simple method to analyze sentiment data. This is a basic example. More algorithms and theories can be used to analyze real-time data.go analyzeSentiment()
to process input data concurrently.Try out this example using more input data and observe the result.
It can be different for your case because,
positive
positive
negative
We have defined goroutines, which will work in the background, but how would we come to know when these goroutines will be completed?
Without a waitgroup, there is no way to know when multiple Goroutines have been completed, which can lead to unexpected results
, race conditions
, and other issues.
Waitgroup is a primitive of Go’s sync package. They provide a way to synchronize the execution of Goroutines and ensure that all Goroutines have been completed before terminating.
It increases the count by 1 when goroutines start executing and decrease it when it completes. Therefore, the main goroutine will wait until the count reaches 0 and then continue to the next execution.
package main
import (
"fmt"
"strings"
"sync"
)
var wg sync.WaitGroup // Initialize waitgroup
func analyzeSentiment(data string, resultChan chan string) {
// Signal that the goroutine has completed its work
defer wg.Done()
if strings.Contains(strings.ToLower(data), "happy") {
resultChan <- "positive"
} else {
resultChan <- "negative"
}
}
func main() {
input := []string{
"I am so happy today!",
"I hate this weather.",
"Happy birthday!!",
}
resultChan := make(chan string)
for _, data := range input {
// Add one to the waitgroup for each goroutine
wg.Add(1)
go analyzeSentiment(data, resultChan)
}
go func() {
// Wait for all goroutines to complete
wg.Wait()
// Close the result channel to signal the workers to terminate
close(resultChan)
}()
// Print the results
for i := 0; i < len(input); i++ {
fmt.Println(<-resultChan)
}
}
In the above code,
var wg sync.WaitGroup
go analyzeSentiment
.wait
until all other goroutines will be completed and closed the channel
to ensure that all the data has been received.wg.Done
in analyzeSentiment
to signal that the goroutine has been completed.During concurrent execution, code may enter the critical section(resource or code accessed by multiple processes). When the values of the critical section depend on the sequence of execution(i.e increasing counter), then it becomes inconsistent and results in the race conditions.
To avoid this, Mutex(mutual exclusion) can be used. It is working on a locking mechanism. When a resource is acquired by one process, add a lock, and after finishing it, unlock it.
For example, In clothes store, when one person is using trial room, he/she will lock the room, try the cloth and unlock the room, other persons will wait till the room is unlocked. Here trial room is critical section.
It’s important to use mutexes carefully, as they can introduce overhead and potentially lead to deadlocks if used incorrectly. However, in situations where multiple goroutines need to access a shared resource, a mutex can be a useful tool for ensuring synchronization and avoiding race conditions.
Mutex is also provided by the sync package.
package main
import (
"fmt"
"strings"
"sync"
)
// Create a mutex to synchronize access to the counter variable
var mu sync.Mutex
var wg sync.WaitGroup
func analyzeSentiment(data string, resultChan chan string, counter *int) {
defer wg.Done()
if strings.Contains(strings.ToLower(data), "happy") {
// Acquire the lock before accessing the shared counter variable
mu.Lock()
*counter++
mu.Unlock()
resultChan <- "positive"
} else {
resultChan <- "negative"
}
}
func main() {
input := []string{
"I am so happy today!",
"I hate this weather.",
"Happy birthday!!",
}
resultChan := make(chan string)
// Create a counter variable to track the number of positive sentiments
counter := 0
for _, data := range input {
wg.Add(1)
go analyzeSentiment(data, resultChan, &counter)
}
go func() {
wg.Wait()
close(resultChan)
}()
// Print the results
for i := 0; i < len(input); i++ {
fmt.Println(<-resultChan)
}
// Print the number of positive sentiments
fmt.Printf("%d out of %d input strings had a positive sentiment.\n", counter, len(input))
}
In the above code,
var mu sync.Mutex
analyzeSentiment
.analyzeSentiment
, used mu.Lock()
to acquire the lock while increasing the counter and releasing the lock with mu.Unlock()
positive
negative
positive
2 out of 3 input strings had a positive sentiment.
A worker is a goroutine that performs a specific task or set of tasks in the background, independently of the main program or other workers.
The basic idea behind workers is to create a pool of goroutines that can be used to perform a set of tasks concurrently. By using them, you can achieve parallelism because workers will take advantage of the available system resources and achieve higher performance and throughput.
package main
import (
"bufio"
"fmt"
"os"
"strings"
"sync"
)
var mu sync.Mutex
var wg sync.WaitGroup
func analyzeSentiment(data string, resultChan chan<- string) {
if strings.Contains(strings.ToLower(data), "happy"){
resultChan <- "positive"
} else {
resultChan <- "negative"
}
}
func worker(inputChan <-chan string, resultChan chan<- string, k int) {
defer wg.Done()
for data := range inputChan {
analyzeSentiment(data, resultChan)
// Acquire the lock to access worker
mu.Lock()
fmt.Printf("Worker %d processed line: %s\n", k, data)
mu.Unlock()
}
}
func main() {
inputChan := make(chan string, 10)
resultChan := make(chan string, 10)
// Launch two worker goroutines to process the sentiment analysis results
for i := 0; i < 2; i++ {
wg.Add(1)
go worker(inputChan, resultChan, i)
}
// Read lines from stdin and send them to the workers
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
inputChan <- line
}
close(inputChan)
go func() {
wg.Wait()
close(resultChan)
}()
numPositive := 0
numNegative := 0
for result := range resultChan {
switch result {
case "positive":
numPositive++
case "negative":
numNegative++
}
}
// Print the results
fmt.Printf("Positive: %d\n", numPositive)
fmt.Printf("Negative: %d\n", numNegative)
}
In the above code, I had taken input from stdin
to analyze sentiment data for efficient use of workers.
inputChan
to store input data in it.2 workers
which will work on inputChan
and analyze data and store results in resultChan
.I am so happy today!
Worker 0 processed line: I am so happy today!
I hate this weather.
Worker 1 processed line: I hate this weather.
Happy birthday!!
Worker 0 processed line: Happy birthday!!
Positive: 2
Negative: 1
Golang has rich support for concurrency using the goroutines
and channels
. In addition, Go provides some default primitives like Mutex
, Waitgroups
, Map
, Once
from the sync package, and timeouts
and cancellations
for controlling concurrent processes.
We’re Grateful to have you with us on this journey!
Suggestions and feedback are more than welcome!
Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.
And with that, we’ll wrap things up for today. Keep learning.
Let's Work Together
Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.