Goroutines Complete Tutorial Explained in Layman's Terms

Getting started with goroutine

In this article, you will be introduced to Goroutines and how to create concurrent programs. In order to learn successfully, ensure you have Go runtime installed in your machine, and you have basic understanding of Go language.

A Goroutine is a very light weight thread that is managed by the Go runtime. Every program in Go has at least one routine called the main Goroutine. A goroutine can be a function or method  that runs independently of the main goroutine. Therefore every light weight thread (Goroutine) works under the main Goroutine and when the main routine is terminated, all present Goroutines in the program will die or terminate.  When the main Goroutine runs concurrently with the other Goroutines, the Goroutines will run in the background simultaneously and in connection with other Goroutines present in the program.

Advertisement

 

Why use Goroutines? How is it different from thread?

  1. Goroutines enable use of concurrency in a much easier way. Concurrency is the ability of a program to run more than one task independently in an overlapping period.
  2. It's extremely cheap to create and use goroutines compared to creating and working with threads. Goroutines are a few kilobytes in size and the stack can grow and shrink according to the needs of the application. On the other hand, thread stack size is fixed and it cannot grow and shrink.
  3. Goroutines communicate using channels, which prevent race conditions when trying to access shared memory using goroutines.

 

Create your first goroutine

When writing an application in order to launch a goroutine, all that is needed is to execute a function preceded by the go keyword:

package main

import (
	"fmt"
)

func main() {
	go fmt.Println("Hello World")
}

Here as you can see I have added a prefix "go" to fmt.Println function so this function will be executed as goroutine. Now if you run this piece of go code, what would happen? You would get an empty output. But Why?

In go, as soon as the application terminates, along with it the goroutine also exits which is what is also happening in this case hence we are not getting any output. What happens is that the Go statements create the goroutine with the respective runtime.g, but this has to be picked up by the Go scheduler, and this does not happen because the program terminates right after the goroutine has been instantiated.

Using the time.Sleep function to let the main goroutine wait (even a nanosecond!) is enough to let the scheduler pick up the goroutine and execute its code. This is shown in the following code:

package main

import (
	"fmt"
	"time"
)

func main() {
	go fmt.Println("Hello World")
	time.Sleep(time.Nanosecond)
}

Output:

Hello World

 

Using golang function and methods as goroutine

We learned earlier that a goroutine is a function or a method. Now that we know how to create a goroutine from our previous example, we can use the same method to convert an our own function or method to goroutine by using keyword go as prefix:

Advertisement

Syntax

func greetings(){
   // Code to execute
}

// Create a goroutine
go greetings()

In the above example, we use the keyword go before the greetings() function to indicate that the greetings() function is a goroutine and therefore our program will have two goroutines. One goroutine is the main goroutine and the second is the greeting() goroutine.

Example

package main
 
import "fmt"
 
func main() {
   greetings()
 
}
 
func greetings() {
   for i := 0; i < 5; i++ {
       fmt.Println(i, " Hello world")
   }
}

Explanation

In the proceeding example, we create a function called greetings() that loops through integer values from 0 to 5 and prints the string “Hello world”. We then call the greetings function inside the main function and the code gets executed. In this example , we are only making use of one goroutine, and that is the main goroutine. This is how a normal program would run if no tasks are needed to run simultaneously with the main goroutine.

Output

$ go run main.go
0  Hello world
1  Hello world
2  Hello world
3  Hello world
4  Hello world

In the next example, we will run two goroutines, the main goroutine and the greetings() goroutine.

Example

package main
 
import "fmt"
 
func main() {
   fmt.Println("Start of main Goroutine")
   go greetings()
   fmt.Println("End of main Goroutine")
 
}
 
func greetings() {
   fmt.Println("Start of greetings Goroutine")
   for i := 0; i < 5; i++ {
       fmt.Println(i, " Hello world")
   }
   fmt.Println("End of greetings Goroutine")
}

Explanation

In the above example, we introduce a new greetings() routine to run alongside the main goroutine. This is made possible by adding the go keyword before the greetings() function. It's worth mentioning that the main goroutine does not use the go keyword to indicate it is a goroutine, Go  runtime does that on the fly.

In the output, we see that the greetings() function is not executed. Why is it so? This is because when the goroutine creates a new go routine(go greetings()), it does not wait for the greetings()  function to execute, and moves to the next line which is fmt.Println(“End of main Goroutine”)

Output

Advertisement
$ go run main.go
Start of main Goroutine
End of main Goroutine

 

goroutine wait for completion

Method-1: Using time.Sleep

We can ensure that the greetings()  function gets executed, by asking the main thread(main Goroutine) to wait for some time, for the greetings() to finish executing. Although this is not a very robust solution but this will do the trick for time being. The better solution would be to use sync.WaitGroup which we will cover in next section.

Example

package main
 
import (
   "fmt"
   "time"
)
 
func main() {
   fmt.Println("Start of main Goroutine")
   go greetings()
   time.Sleep(time.Second * 3)
   fmt.Println("End of main Goroutine")
 
}
 
func greetings() {
   fmt.Println("Start of greetings Goroutine")
   for i := 0; i < 5; i++ {
       fmt.Println(i, " Hello world")
   }
   fmt.Println("End of greetings Goroutine")
}

Explanation

To delay execution , we  use the time package to force the main goroutine to sleep for 3 seconds and continue only when the three seconds are over. Suppose the sleep time is reduced, the main goroutine will not wait for the greetings() function to finish. The main routine will terminate/die and the same will apply to the greetings() function.

Output

$ go run main.go
Start of main Goroutine
Start of greetings Goroutine
0  Hello world
1  Hello world
2  Hello world
3  Hello world
4  Hello world
End of greetings Goroutine
End of main Goroutine

 

Method-2: Using sync.WaitGroup

WaitGroups are commonly used in order to validate the fact that multiple goroutines have completed. We do this in order to make sure we have completed all of the concurrent work that we expect to complete.

Advertisement

In the example in the following code block, we make requests to four websites with a WaitGroup. This sync.WaitGroup will wait until all of our requests have been completed, and will only finish the main function after all of the WaitGroup values have been returned:

package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

func retrieve(url string, wg *sync.WaitGroup) {
	// WaitGroup Counter-- when goroutine is finished
	defer wg.Done()
	start := time.Now()
	res, err := http.Get(url)
	end := time.Since(start)
	if err != nil {
		panic(err)
	}
	// print the status code from the response
	fmt.Println(url, res.StatusCode, end)

}

func main() {
	var wg sync.WaitGroup
	var urls = []string{"https://godoc.org", "https://www.packtpub.com", "https://kubernetes.io/"}
	for i := range urls {
		// WaitGroup Counter++ when new goroutine is called
		wg.Add(1)
		go retrieve(urls[i], &wg)
	}
	// Wait for the collection of goroutines to finish
	wg.Wait()
}

As you can see from the following output, we receive all the measurements for the web requests, their response code, and their respective timings:

$ go run main.go
https://www.packtpub.com 200 93.520174ms
https://kubernetes.io/ 200 124.9645ms
https://godoc.org 200 746.245288ms

Very often, we expect all our goroutines to finish. WaitGroups can help us with this.

 

Using two goroutines concurrently

Your program can have as many goroutines running concurrently as you want. For example, a machine with 4GB of memory can spin less than 1 million goroutines , considering that a goroutine takes a few kbs. In this example, we will demonstrate using two goroutines.

Example

package main
 
import (
   "fmt"
   "time"
)
 
func main() {
   fmt.Println("Start of main Goroutine")
   go greetings("John")
   go greetings("Mary")
   time.Sleep(time.Second * 10)
   fmt.Println("End of main Goroutine")
 
}
 
func greetings(name string) {
   for i := 0; i < 3; i++ {
       fmt.Println(i, "==>", name)
       time.Sleep(time.Millisecond)
  }
}

Explanation

Advertisement

In the main function, we create two goroutines, one prints the string “John” and the other prints the string “Mary”. As indicated in the output, the two goroutines are independent of each other in their own runtime environment at the same time in connection with the main function. The main goroutines sleep for 3 seconds, giving time for the goroutines to finish executing their code.

Output

$ go run main.go
Start of main Goroutine
0 ==> Mary
0 ==> John
1 ==> Mary
1 ==> John
2 ==> John
2 ==> Mary
End of main Goroutine

 

Anonymous goroutine

In the previous examples, we have worked with named goroutines. Well it is possible to create an anonymous goroutine that does not require the name of the goroutine. To create an anonymous goroutine, use the go keyword go followed by the func keyword as shown below where we create a trap function to trap for some of the known exit signals to handle abrupt termination of the code:

Example

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

var pipe *os.File

// Create channel to capture termination signal
func trapInterrupts(pipe *os.File) {

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

        // Create goroutine
	go func() {
		<-sigs
		fmt.Println("Kill signal received")
                // Do something when kill signal received
		pipe.Close()
		os.Exit(0)
	}()
}

func main() {
	// Trap interrupts to perform cleanup
	trapInterrupts(pipe)

	fmt.Println("Starting read operation")
	pipe, err := os.OpenFile("/tmp/myfile.txt", os.O_RDONLY, 0640)
	if err != nil {
		fmt.Println("Couldn't open pipe with error: ", err)
	}
	defer pipe.Close()

	// Do something with pipe

}

 

Running two concurrent servers using goroutines

In this example, we will spin two servers, one will server on port 8081 and another on port 8080. This is made possible by running two goroutines while the main goroutines run infinitely.

Example

Advertisement
package main
 
import (
   "fmt"
   "log"
   "net/http"
   "time"
)
func main() {
   go server("8080", "v1")
   go server("8081", "v2")
   for {
       log.Println("Server running")
       time.Sleep(time.Second * 5)
   }
}
func server(port, version string) {
   route := fmt.Sprintf("/%s", version)
   port = fmt.Sprintf(":%s", port)
   http.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
       fmt.Println("Request to server version ", version)
       fmt.Fprintf(w, "API %s running", route)
   })
   log.Fatal(http.ListenAndServe(port, nil))
}

Explanation

In this example, we create a server function, and we call it twice with different port numbers and version number of the API endpoint. A client can make GET requests to these servers running concurrently by hitting these endpoints http://127.0.0.1:8080/v1 and http://127.0.0.1:8080/v2. The main goroutines run indefinitely until a keyboard interrupt signal is sent to it. This allows for the servers to listen to requests and log the events in the terminal.

Output

$ go run main.go
2022/08/07 15:16:53 Server running
Request to server version  v1
2022/08/07 15:16:58 Server running
Request to server version  v2
2022/08/07 15:17:03 Server running
^Csignal: interrupt

 

Summary

Goroutines are light weight threads that are defined as functions or methods in Go.  This enables our program to run multiple tasks without blocking one another at the same time. In this article we have explored how to  create a goroutine, run single and multiple goroutines.

 

References

https://go.dev/tour/concurrency/1
https://golangbot.com/goroutines/

 

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X