Hello!
The topic of today’s post is goroutines. We will learn what they are and how to use them.
Goroutine
A Goroutine allows you to run code concurrently. For example, we can call several functions at the same time without waiting for each other. To create a goroutine before calling the function, add the go
keyword.
package main
import "fmt"
func main() {
go hello()
}
func hello() {
fmt.Println("Hello!")
}
But if run this code, nothing will happen. Because the main function is not going to wait for function hello to complete its work, and it will just close the program. We can add sleep, to wait for the hello function. But this is not the best approach. Because we don’t know for sure how long it will take to complete the function. There is another way how the go can wait for goroutine, using WaitGroup.
WaitGroup
WaitGroup allows you to wait for the execution of a goroutine. To do this, we create the variable wg
, which has the value sync.WaitGroup{}
. And later in the code before calling the goroutine, we call wg.Add(1)
to add one goroutine to the waiting list. After the goroutine call, we need to add wg.Wait()
to wait for execution. And in the goroutine itself, you need to add wg.Done()
to remove the previously added goroutine from the waiting list. And now when we call the code we will get the result.
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
wg.Add(1)
go hello()
wg.Wait()
}
func hello() {
fmt.Println("Hello!")
wg.Done()
}
Mutex
Let’s try to run two goroutines that will simultaneously write/read from the same variable.
In this example, goroutines are executed in a loop. One of the functions increments the value of the counter
by 1, and the other print the value.
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
var counter = 0
func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
go readCounter()
go increment()
}
wg.Wait()
}
func increment() {
counter++
wg.Done()
}
func readCounter() {
fmt.Println(counter)
wg.Done()
}
After running this code, we will get the following result
$ go run main.go
0
2
3
5
4
6
6
7
7
9
Not exactly what we expected, and if you restart the code, the result will be different. This is because a race condition occurs and all goroutines try to change one object. If you run the code with the -race
key, you will see the following error
$ go run -race main.go
0
3
1
1
1
1
1
==================
WARNING: DATA RACE
Write at 0x000104f55a88 by goroutine 10:
main.increment()
/Users/maksym/go/src/github.com/mpostument/hello/main.go:22 +0x3c
Previous read at 0x000104f55a88 by goroutine 7:
main.readCounter()
/Users/maksym/go/src/github.com/mpostument/hello/main.go:27 +0x2c
Goroutine 10 (running) created at:
main.main()
/Users/maksym/go/src/github.com/mpostument/hello/main.go:15 +0x50
Goroutine 7 (finished) created at:
main.main()
/Users/maksym/go/src/github.com/mpostument/hello/main.go:14 +0x44
==================
3
3
5
Found 1 data race(s)
exit status 66
How can this be solved? Using Mutex. We can allow access to our object for only one goroutine. In the example, we will use RWMutex
, which allows any number of goroutines to read data and write for only one. Before the goroutine that reads data, add RLock, and before the one that writes, add Lock. In the function itself, after the writing/reading has taken place, remove the lock. And now if we run the code, we will get the expected result.
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
var mutex = sync.RWMutex{}
var counter = 0
func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
mutex.RLock()
go readCounter()
mutex.Lock()
go increment()
}
wg.Wait()
}
func increment() {
counter++
mutex.Unlock()
wg.Done()
}
func readCounter() {
fmt.Println(counter)
mutex.RUnlock()
wg.Done()
}
$ go run -race main.go
0
1
2
3
4
5
6
7
8
9