Goroutines are one of the most interesting features that Go has. They are lightweight threads of execution that allow us to program concurrent applications without having to deal with complex threads or parallelism since everything is handled by the Go runtime.

Let’s play a bit with it so we can see different characteristics.

We’ll start with this simple hello world application:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello, playground")
}

Output:

Hello, playground

How to spin up a goroutine

It is as easy as adding go before the function call:

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

Output:

So, why is it not returning anything now?

Since a goroutine is like a new thread, the program is not waiting for the goroutine to finish; we have to tell the main thread to wait for the goroutine.

Let’s add a sleep to be sure that is what we’re saying:

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		fmt.Println("Hello, playground")
	}()
	time.Sleep(time.Second)
}

Output:

Hello, playground

Now it is working! But we can’t be adding sleeps since we can’t know how much time any goroutine will take.

Waiting for the goroutine to finish

To do so we have several ways, let’s review all of them.

WaitGroups

WaitGroups allow us to wait until all the goroutines have finished. They are very easy to use.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	
	wg.Add(1)
	
	go func() {
		defer wg.Done()
		fmt.Println("Hello, playground")
	}()
	
	wg.Wait()
}

Output:

Hello, playground

fatal error: all goroutines are asleep - deadlock!

This is a very typical error we might get when working with WaitGroups. It happens when the program reaches wg.Wait() while the WaitGroup counter is still greater than zero (because wg.Add(1) was called without a matching wg.Done()), and all other goroutines have already finished.

Example of the error:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	
	wg.Add(1)
	
	go func() {
		fmt.Println("Hello, playground")
	}()
	
	wg.Wait()
}

Output:

Hello, playground
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000100018)
	/usr/local/go-faketime/src/runtime/sema.go:56 +0x45
sync.(*WaitGroup).Wait(0xc000100010)
	/usr/local/go-faketime/src/sync/waitgroup.go:130 +0x65
main.main()
	/tmp/sandbox871907795/prog.go:17 +0x73

Channels

Channels are a tool that allow us to communicate between goroutines. For example we can have one goroutine sending information to a channel and another one reading from it.

In this example we’ll use a channel to make the main thread wait for the goroutine since an unbuffered channel will always wait until data is received.

package main

import (
	"fmt"
)

func main() {
	done := make(chan bool)

	
	go func() {
		fmt.Println("Hello, playground")
		
		done <- true
	}()
	
	<- done
}

Output:

Hello, playground

Resources