Suppose that Go program starts two goroutines:

package main import (

"fmt"

"sync"

) func main() {

var v int

var wg sync.WaitGroup

wg.Add(2)

go func() {

v = 1

wg.Done()

}()

go func() {

fmt.Println(v)

wg.Done()

}()

wg.Wait()

}

Both goroutines operates on shared variable v. One of them sets new value (writes) and the second one prints that variable (reads).

WaitGroup from sync package is used to wait till two non-main goroutines terminate. Otherwise there wouldn’t any guarantee that any of those goroutines would even start.

Since goroutines are independent concurrent tasks, there is no implicit ordering between operations they run. In the above example it‘s not clear if 0 or 1 will be printed. Output will be 1 when at the moment fmt.Println is fired, the other gorutine already executed assignment statement v = 0 . It’s unknown though until the program will actually run. It other words assignment statement and call to fmt.Println aren’t ordered — they‘re concurrent.

It’s not good if we can’t tell what program does by looking at its source code. Go’s specification introduces partial order (happens before) of the memory operations (reads or writes). This order enables to deduce what program does. Certain mechanisms in the language additionally allow programer to enforce order of operations.

Inside single goroutine all operations are ordered as they’re placed in the source code:

wg.Add(2)

wg.Wait()

Function calls from the above example since placed inside the same goroutine are ordered — wg.Add(2) happens before wg.Wait() .

1. Channels

Communication using channels is the primary method for synchronization. Sending value to a channel happens before receiving it:

var v int

var wg sync.WaitGroup

wg.Add(2)

ch := make(chan int)

go func() {

v = 1

ch <- 1

wg.Done()

}()

go func() {

<-ch

fmt.Println(v)

wg.Done()

}()

wg.Wait()

The new thing is ch channel. Since receiving happens after sending value to a channel and sending value happens after assigning to v then above program prints always 1 :

set v → send to ch → receive from ch → print v

First and third arrow are consequences of ordering within the same goroutine. Using channel communication introduced second arrow. Ultimately operations spread across two goroutines are ordered.

2. sync package

Package sync provides synchronization primitives. One of them which could solve our problem is Mutex. Having a variable lock of type sync.Mutex it’s guaranteed that 2nd call to lock.Lock() happens after 1st call to lock.Unlock() . 3rd call to lock.Lock() happens after 1st and 2nd calls to lock.Unlock() . Generally speaking nth call to lock.Lock() happens after mth call to lock.Unlock() for m < n. Let’s see how to use this knowledge for our synchronization problem: