Goroutines

In Go, we achieve concurrency by working with Goroutines. They are like processes that run applications in a computer concurrently; in fact, the main loop of Go could be considered a Goroutine, too. Goroutines are used in places where we would use actors. They execute some logic and die (or keep looping if necessary).

But Goroutines are not threads. We can launch thousands of concurrent Goroutines, even millions. They are incredibly cheap, with a small growth stack. We will use Goroutines to execute code that we want to work concurrently. For example, three calls to three services to compose a response can be designed concurrently with three Goroutines to do the service calls potentially in parallel and a fourth Goroutine to receive them and compose the response. What's the point here? That if we have a computer with four cores, we could potentially run this service call in parallel, but if we use a one-core computer, the design will still be correct and the calls will be executed concurrently in only one core. By designing concurrent applications, we don't need to worry about parallel execution.

Returning to the bike analogy, we were pushing the pedals of the bike with our two legs. That's two Goroutines concurrently pushing the pedals. When we use the tandem, we had a total of four Goroutines, possibly working in parallel. But we also have two hands to handle the front and rear brakes. That's a total of eight Goroutines for our two threads bike. Actually, we don't pedal when we brake and we don't brake when we pedal; that's a correct concurrent design. Our nervous system transports the information about when to stop pedaling and when to start braking. In Go, our nervous system is composed of channels; we will see them after playing a bit with Goroutines first.

Our first Goroutine

Enough of the explanations now. Let's get our hands dirty. For our first Goroutine, we will print the message Hello World! in a Goroutine. Let's start with what we've been doing up until now:

package main 
 
func main() { 
  helloWorld() 
} 
 
func helloWorld(){ 
  println("Hello World!") 
} 

Running this small snippet of code will simply output Hello World! in the console:

$ go run main.go
Hello World!

Not impressive at all. To run it in a new Goroutine, we just need to add the keyword go at the beginning of the call to the function:

package main 
 
func main() { 
  go helloWorld() 
} 
 
func helloWorld(){ 
  println("Hello World!") 
} 

With this simple word, we are telling Go to start a new Goroutine running the contents of the helloWorld function.

So, let's run it:

$ go run main.go 
$

What? It printed nothing! Why is that? Things get complicated when you start to deal with concurrent applications. The problem is that the main function finishes before the helloWorld function gets executed. Let's analyse it step by step. The main function starts and schedules a new Goroutine that will execute the helloWorld function, but the function isn't executed when the function finishes--it is still in the scheduling process.

So, our main problem is that the main function has to wait for the Goroutine to be executed before finishing. So let's pause for a second to give some room to the Goroutine:

package main 
import "time" 
 
func main() { 
  go helloWorld() 
 
  time.Sleep(time.Second) 
} 
 
func helloWorld(){ 
  println("Hello World!") 
} 

The time.Sleep function effectively sleeps the main Goroutine for one second before continuing (and exiting). If we run this now, we must get the message:

$ go run main.go
Hello World!

I suppose you must have noticed by now the small gap of time where the program is freezing before finishing. This is the function for sleeping. If you are doing a lot of tasks, you might want to raise the waiting time to whatever you want. Just remember that in any application the main function cannot finish before the rest of the Goroutines.

Anonymous functions launched as new Goroutines

We have defined the helloWorld function so that it can be launched with a different Goroutine. This is not strictly necessary because you can launch snippets of code directly in the function's scope:

package main 
import "time" 
 
func main() { 
  go func() { 
    println("Hello World") 
  }() 
  time.Sleep(time.Second) 
} 

This is also valid. We have used an anonymous function and we have launched it in a new Goroutine using the go keyword. Take a closer look at the closing braces of the function-they are followed by opening and closing parenthesis, indicating the execution of the function.

We can also pass data to anonymous functions:

package main 
import "time" 
 
func main() { 
  go func(msg string) { 
    println(msg) 
  }("Hello World") 
  time.Sleep(time.Second) 
} 

This is also valid. We had defined an anonymous function that received a string, which then printed the received string. When we called the function in a different Goroutine, we passed the message we wanted to print. In this sense, the following example would also be valid:

package main 
import "time" 
 
func main() { 
  messagePrinter := func(msg string) { 
    println(msg) 
  } 
 
  go messagePrinter("Hello World") 
  go messagePrinter("Hello goroutine") 
  time.Sleep(time.Second) 
} 

In this case, we have defined a function within the scope of our main function and stored it in a variable called messagePrinter. Now we can concurrently print as many messages as we want by using the messagePrinter(string) signature:

$ go run main.go
Hello World
Hello goroutine

We have just scratched the surface of concurrent programming in Go, but we can already see that it can be quite powerful. But we definitely have to do something with that sleeping period. WaitGroups can help us with this problem.

WaitGroups

WaitGroup comes in the synchronization package (the sync package) to help us synchronize many concurrent Goroutines. It works very easily--every time we have to wait for one Goroutine to finish, we add 1 to the group, and once all of them are added, we ask the group to wait. When the Goroutine finishes, it says Done and the WaitGroup will take one from the group:

package main 
 
import ( 
  "sync" 
  "fmt" 
) 
 
func main() { 
  var wait sync.WaitGroup 
  wait.Add(1) 
 
  go func(){ 
    fmt.Println("Hello World!") 
    wait.Done() 
  }() 
  wait.Wait() 
} 

This is the simplest possible example of a WaitGroup. First, we created a variable to hold it called the wait variable. Next, before launching the new Goroutine, we say to the WaitGroup hey, you'll have to wait for one thing to finish by using the wait.Add(1) method. Now we can launch the 1 that the WaitGroup has to wait for, which in this case is the previous Goroutine that prints Hello World and says Done (by using the wait.Done() method) at the end of the Goroutine. Finally, we indicate to the WaitGroup to wait. We have to remember that the function wait.Wait() was probably executed before the Goroutine.

Let's run the code again:

$ go run main.go 
Hello World!

Now it just waits the necessary time and not one millisecond more before exiting the application. Remember that when we use the Add(value) method, we add entities to the WaitGroup, and when we use the Done() method, we subtract one.

Actually, the Add function takes a delta value, so the following code is equivalent to the previous:

package main 
 
import ( 
  "sync" 
  "fmt" 
) 
 
func main() { 
  var wait sync.WaitGroup 
  wait.Add(1) 
 
  go func(){ 
    fmt.Println("Hello World!") 
    wait.Add(-1) 
  }() 
  wait.Wait() 
} 

In this case, we added 1 before launching the Goroutine and we added -1 (subtracted 1) at the end of it. If we know in advance how many Goroutines we are going to launch, we can also call the Add method just once:

package main 
import ( 
  "fmt" 
  "sync" 
) 
 
func main() { 
  var wait sync.WaitGroup 
 
  goRoutines := 5 
  wait.Add(goRoutines) 
 
  for i := 0; i < goRoutines; i++ { 
    go func(goRoutineID int) { 
      fmt.Printf("ID:%d: Hello goroutines!
", goRoutineID) 
      wait.Done() 
    }(i) 
  } 
  wait.Wait() 
} 

In this example, we are going to create five Goroutines (as stated in the goroutines variable). We know it in advance, so we simply add them all to the WaitGroup. We are then going to launch the same amount of goroutine variables by using a for loop. Every time one Goroutine finishes, it calls the Done() method of the WaitGroup that is effectively waiting at the end of the main loop.

Again, in this case, the code reaches the end of the main function before all Goroutines are launched (if any), and the WaitGroup makes the execution of the main flow wait until all Done messages are called. Let's run this small program:

$ go run main.go 

ID:4: Hello goroutines!
ID:0: Hello goroutines!
ID:1: Hello goroutines!
ID:2: Hello goroutines!
ID:3: Hello goroutines!

We haven't mentioned it before, but we have passed the iteration index to each Goroutine as the parameter GoroutineID to print it with the message Hello goroutines! You might also have noticed that the Goroutines aren't executed in order. Of course! We are dealing with a scheduler that doesn't guarantee the order of execution of the Goroutines. This is something to keep in mind when programming concurrent applications. In fact, if we execute it again, we won't necessarily get the same order of output:

$ go run main.go
ID:4: Hello goroutines!
ID:2: Hello goroutines!
ID:1: Hello goroutines!
ID:3: Hello goroutines!
ID:0: Hello goroutines!
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.214.30