Let’s talk about goroutines.
But wait, let’s first look into concurrency, what is concurrency in programming?
Concurrency in programming refers to the ability of a system to handle multiple tasks or operations simultaneously.
Frankly, this definition could benefit from simplification. Let’s keep it simple and understand it with an example.
Let’s say you need to make roti, sabzi, dal, and rice (A common meal in an Indian household) as soon as possible. You will probably start with tasks like.
- Soaking the dal and rice in water.
- Then, you’ll start kneading the dough for roti.
- Meanwhile, you might start cutting the vegetables.
- Next, you might give some tadka for the dal and start cooking it. — And, then you might start sauteing vegetables.
- Making the roti, and so on.
Eventually, you will complete making all 4 items.
You had primarily 4 tasks making roti, sabzi dal, and rice. But instead of starting and completing one task before moving on to the next, you tried to do them simultaneously, and it made sense because you had to finish as soon as possible.
The same goes for programming. Concurrency is important because it helps us perform multiple tasks simultaneously.
Note: Unlike the example, concurrency is not necessarily helping you to make things fast, it’s actually parallelism that helps us with that. If a concurrent program has to run on only one core, it might run slower. But let’s be real — those days are long gone for most use cases.
However, there are cases where concurrency can speed things up. For example, when you have to wait for an external event, like a server response, your program can do something else while waiting. (By the way, this is the entire concept of Node.js and why it performs great for I/O tasks.)
Getting back to the topic.
We don’t really have control over the order of execution of processes in concurrent programs, and in a certain sense, if there’s no race condition, who cares as long as all our tasks are completed by the end of the day? Just like in the example, it doesn’t matter whether you knead the dough first and then cut the vegetables, or do it in reverse order.
But what if there are race conditions? In that case, we do need to be careful and ensure that tasks that must occur in a specific order are grouped together while executing multiple tasks concurrently.
Go Routines
Check out this example.
We have a list of IDs and we need to get user details based on IDs.
For simplicity, I have kept a fake db call so that we can focus on routines instead
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
ids := []int{1, 2, 3, 4, 5}
for _, id := range ids {
getUserById(id)
}
fmt.Println("All tasks completed !")
}
func getUserById(id int) {
// fake Db call.
delay := rand.Float32() * 3000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Printf("Query executed for %v\n", id)
}When you run this you will get the following output.
Each line will get printed after the given delay, that is sequentially.
Get Saumyadixit07’s stories in your inbox
Join Medium for free to get updates from this writer.
But this isn’t ideal. This is where routines can help.
Just use the “go” keyword in front of the function you want to run concurrently.
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
ids := []int{1, 2, 3, 4, 5}
for _, id := range ids {
go getUserById(id) // added go key in front of the function call
}
fmt.Println("All tasks completed !")
}
func getUserById(id int) {
// Fake Db call.
delay := rand.Float32() * 3000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Printf("Query executed for %v\n", id)
}And voila, here we have concurrent execution in go. Run it.
But wait, the log inside of the db call is not printed. Why? Because our program spawned this task in the background and did not wait for them to complete.
Here is where weight groups come into the picture. You can think of them as trackers. We add and remove values from them and wait for the weight group to be back to 0 so that we know all our tasks are completed and we can execute the rest of the program.
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
ids := []int{1, 2, 3, 4, 5}
wg := sync.WaitGroup{}
for _, id := range ids {
wg.Add(1)
go getUserById(id, &wg)
}
wg.Wait()
fmt.Println("All tasks completed !")
}
func getUserById(id int, wg *sync.WaitGroup) {
// ake Db call.
delay := rand.Float32() * 3000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Printf("Query executed for %v\n", id)
wg.Done()
}Run the code, and you’ll obtain the output much faster.
All cool, but we haven’t really achieved our objective yet.
We are making a db call but not really storing the result. Assume that we have a slice inside of our function (this is done to simulate an issue that we might encounter) and we need to append to that slice whatever comes from the db call. At times it is possible that multiple threads try to write to the same shared memory. In this case our slice.
Have a look at this code.
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
ids := []int{1, 2, 3, 4, 5}
result := []string{"zxmcaa"}
wg := sync.WaitGroup{}
for _, id := range ids {
wg.Add(1)
go getUserById(id, &wg, &result)
}
wg.Wait()
fmt.Printf("All tasks completed ! and the the updated result is : %v\n", result)
}
func getUserById(id int, wg *sync.WaitGroup, result *[]string) {
// fake Db call.
// created the issue by making sure that all threads tries to write to the memory at the same time.
delay := 1000
time.Sleep(time.Duration(delay) * time.Millisecond)
// updating the slice.
randomName := generateRandomName(5)
*result = append(*result, randomName)
fmt.Printf("Query executed for %v\n", id)
wg.Done()
}
func generateRandomName(length int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, length)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
}
return string(b)
}The output will be something like this.
Interesting! We get 5 items in our slice. But wait we should have 6 items as we have 5 id’s. What just happened?
Try running the code multiple times if you don’t see the issue.
Basically, multiple threads tried to write to the same memory causing corrupt memory. So now what?
We can create a mutual exclusion or mutex and use mutex lock and mutex unlock to avoid routines to write to shared memory.
- When a goroutine reaches mutex.Lock(), attempts to acquire the lock. If no other goroutine has locked the mutex, it will succeed, and the goroutine can proceed with executing the code that follows.
- If another goroutine has already locked the mutex, the current goroutine will be blocked and forced to wait until the mutex becomes available (i.e., when the other goroutine calls mutex.Unlock()).
- By ensuring that only one goroutine can access the critical section at a time, you prevent race conditions where multiple goroutines might attempt to read or write shared resources simultaneously, leading to unexpected behavior.
- The mutex ensures that shared data remains consistent, as it is not simultaneously modified by multiple goroutines. Here is how to implement them in code.
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
ids := []int{1, 2, 3, 4, 5}
result := []string{"zxmcaa"}
wg := sync.WaitGroup{}
mut := sync.Mutex{}
for _, id := range ids {
wg.Add(1)
go getUserById(id, &wg, &result, &mut)
}
wg.Wait()
fmt.Printf("All tasks completed ! and the the updated result is : %v\n", result)
}
func getUserById(id int, wg *sync.WaitGroup, result *[]string, mut *sync.Mutex) {
// fake Db call.
// created the issue by making sure that all processes tries to write to the memory at the same time.
delay := 1000
time.Sleep(time.Duration(delay) * time.Millisecond)
// updating the slice.
randomName := generateRandomName(5)
mut.Lock()
*result = append(*result, randomName)
mut.Unlock()
fmt.Printf("Query executed for %v\n", id)
wg.Done()
}
func generateRandomName(length int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, length)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
}
return string(b)
}Output !
I hope you guys got some idea about the basics of go routines and some potential problems you might face.
Special thanks to Alex Mux for the inspiration behind this guide. https://youtu.be/8uiZC0l4Ajw?si=prFuDPbTmM-vtXzI





