Table of Contents
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.
Why use Goroutines? How is it different from thread?
- 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.
- 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.
- 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:
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
$ 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.
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
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
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/