We can think of a goroutine, as a lightweight thread. When a new program starts by calling main it starts in a goroutine, conveniently called the main goroutine. We can invoke a function in a new goroutine by prefixing the function call by the keyword go.

Let’s start with a simple program that prints a count up with a delay. No concurrency here yet.

func main() {
	printCountUp("A")
	printCountUp("B")
}

func printCountUp(prefix string) {
	for i := 0; i < 10; i++ {
		fmt.Printf("%s-%d ", prefix,  i)
		time.Sleep(100 *  time.Millisecond)
	}
}

Running this outputs the expected result: A-0 A-1 A-2 A-3 A-4 A-5 A-6 A-7 A-8 A-9 B-0 B-1 B-2 B-3 B-4 B-5 B-6 B-7 B-8 B-9

Let’s introduce the go keyword

go printCountUp("A")    // New goroutine
printCountUp("B")       // Still in main goroutine

Now we get B-0 A-0 A-1 B-1 A-2 B-2 A-3 B-3 A-4 B-4 A-5 B-5 A-6 B-6 A-7 B-7 A-8 B-8 A-9 B-9 Which is expected and the program would run almost at half the time. Now let’s go a bit further

go printCountUp("A")    // New goroutine
go printCountUp("B")    // New goroutine

The program will terminate immediately and we will get no output. The reason being that the parent goroutine, the main goroutine in this case, has terminated and took its children down with it.

There are many reasons why you would, in a real-world program, start multiple goroutines at the same time and wait for them all to finish. For that, we have a WaitGroup. Let’s look at a better implementation.

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	
	go func() {
		defer wg.Done()
		printCountUp("A")
	}()
	go func() {
		defer wg.Done()
		printCountUp("B")
	}()
	
	wg.Wait()
}

This prints B-0 A-0 A-1 B-1 A-2 B-2 A-3 B-3 A-4 B-4 A-5 B-5 A-6 B-6 A-7 B-7 A-8 B-8 A-9 B-9 as expected. Note that we can call wg.Add twice with the argument set to 1 each time. It is just the number of goroutines to wait for.

If we would have called wg.Add(1) only once, we would have finished execution whenever one of the two goroutines finished executing. If we would have used wg.Add(3), we would have gotten a deadlock exception fatal error: all goroutines are asleep - deadlock!.


Resources

  1. The Go Programming Language
  2. How to Wait for All Goroutines to Finish Executing Before Continuing