Skip to content

A simple introduction to the usage of goroutines and go channels in Go

Alexandre Beslic
7 min read
A simple introduction to the usage of goroutines and go channels in Go

Go is a nice programming language in order to introduce beginners to concurrent programming. This post is a very simple introduction to the usage of goroutines and go channels. We will go through the creation of a simple pomodoro tool. Note that this article is not meant to be an introduction to Go. I assume you already have basic knowledge of the Go syntax but you want to have a simple usage example of two new Go language constructs, namely goroutines and Go channels. If you need to learn Go, I invite you to go through the Go Tutorial first.

Goroutines

Think of a goroutine as a thread, concept which you are probably already familiar with other programming languages. In Go, you can spawn a new goroutine to execute code concurrently with the go keyword :

package main
    
import "fmt"
    
func myFunc() {
    // Doing something concurrently
    for i := 0; i < 10; i++ {
        time.Sleep(time.Millisecond * 100)
        fmt.Println(i)
    }
}
    
func main() {
    go myFunc() // myFunc will run concurrently to the main implicit goroutine
      
    // Cheat and wait for user input to wait for the completion of the concurrent goroutine
    var input string
    fmt.Scanln(&input)
}

If you are already familiar with concurrent programming in either C or Java, you might be immediatly struck by the simplicity compared to these languages. Indeed spawning a new goroutine is hassle free and you can harness the power of concurrency with a very low learning curve. Running a function concurrently is as simple as calling go myFunc().

Now try to launch the program with go run example.go and you will notice that there is an issue. If you type Enter before myFunc completes the loop in its own goroutine, the program will stop. Unfortunately, the main implicit goroutine does not wait for the other goroutines to complete their tasks. We need a way to do this in an elegant way instead of cheating by waiting for user input...

Go Channels

Now that we covered the creation of new concurrent goroutines, it can be interesting to let those goroutines communicate to each other. For example a newly created goroutine can tell to the main implicit goroutine that a task is done. For this purpose, we are going to use go channels:

package main
    
import "fmt"
    
func myFunc(done chan string) {
    // Doing something in parallel
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
    fmt.Println("Hey! I do useless stuff!")
    done <- "I'm done!" // We send a message on the channel
}
    
func main() {
    done := make(chan string)
    
    go myFunc(done)
        
    msg := <- done // Block until we receive a message on the channel
    fmt.Println(msg)
        
    fmt.Println("Message received, you were indeed useless..")
}

And here it is, we created a trivial synchronization mechanism. The main goroutine will block until it receives a message from myFunc which is executing in its own goroutine. Better than our dirty cheat from the first example!

Go Channels can be buffered or unbuffered. In this article, we only cover the case of unbuffered go channels. This means that for a goroutine to send a message on a channel, another goroutine must be waiting to receive that message. For a very basic example of a buffered go channel, read this.

Exercise: A simple pomodoro tool

Now let's see how we could use goroutines and go channels to program a simple pomodoro tool.

If you are unfamiliar with Pomodoro®, it is a technique designed to work effectively on a task without being distracted. You begin a pomodoro turn that last 25 minutes and you made an oath with yourself not to go on social media or do any other distracting activity (even looking at mails). When the 25 minutes timer ends, you have a short 5 minutes break to do whatever you want: check twitter, mails, post an update about why you are an amazing person on facebook. When the short break ends you are back in the business for another pomodoro turn of 25 minutes on a single task. After 4 or 5 pomodoro turns, you are rewarded with a long break of 30 minutes and you could read some articles on Hacker News or just chill and drink a good coffee.

Your mission

Develop a simple pomodoro tool using goroutines and go channels in Go. The program will be composed of 5 goroutines. One goroutine is the main program (which is implicit when you launch the program), another is for a pomodoro turn, two goroutines respectively for the short break and the long break and finally the last goroutine will be used to handle messages received from other goroutines.

Turn/Break/Long Break goroutines are notifying the user (with fmt.Printf or any other method) that a Turn/Break/Long Break began and they sleep using time.Sleep for their respective amount of time. Each goroutine will have a dedicated channel used to communicate with the pomodoro service/handler when they are waken up from sleep. The channel is used to inform the service that the timer ended.

The pomodoro service must take appropriate actions when it receives a message from one goroutine and launch another one based on those sequence of actions:

  • Launch a Break goroutine if this is the end of a Pomodoro Turn
  • Launch a Pomodoro Turn if this is the end of a Small Break or a Long Break.
  • Launch a Long Break if we finished our fifth Pomodoro Turn

When we hit the end of the timer for the Long Break we ask the user if he wants to continue. If the answer is No, the service goroutine must inform the main goroutine that it could end the execution of the program. If the answer is Yes, we take the appropriate action and lauch a new Pomodoro Turn.

You have the time of a pomodoro (25 minutes) to complete this task. GO! GO! GO!

A possible solution

First let's begin with the main function (implicit goroutine) :

package main
    
import (
    "fmt"
    "os/exec"
    "time"
)
	    
var(
    currentTurn = 1
    totalTurns =  5
)
    
func main() {
    turn := make(chan bool)
    smallBreak := make(chan bool)
    longBreak := make(chan bool)
    done := make(chan bool)

    go pomodoroTurn(turn)
    go pomodoroService(turn, smallBreak, longBreak, done)

    <-done
}

The main function creates a few go channels that are going to be used by goroutines to send messages. Then we launch the first Pomodoro Turn by firing a goroutine with go pomodoroTurn(turn). Notice that we pass the channel to the goroutine as an argument. Why? Look at the next call go pomodoroService(turn, smallBreak, longBreak, done). The pomodoro service acting as the core of the execution also takes the turn channel. Both are taking turn as an argument because this will be needed for the two goroutines to send messages to each other. When pomodoroTurn will end its execution, it will inform pomodoroService by sending a message on the turn channel. pomodoroService will receive the message and will act accordingly by launching a Small Break.

Now for the <-done part, this means that the main goroutine is waiting synchronously to a message on the done channel. It will block indefinitely until a message is received from pomodoroService.


For now, we have 3 launched goroutines. One is the main goroutine (implicit at launch), another is a pomodoro turn and the last one is the pomodoro service which is looping endlessly to receive messages from other goroutines.


Let's see the rest of the code with the simple case of the first three goroutines: pomodoroTurn, pomodoroBreak and pomodoroLongBreak.

func pomodoroTurn(chanPomodoro chan bool) {
    tellBeginTurn()
    time.Sleep(time.Minute * 25) // Replace *time.Minute* by *time.Second* for quicker testing
    tellEndTurn()
    chanPomodoro <- true
}

func pomodoroBreak(chanBreak chan bool) {
    tellBeginSmallBreak()
    time.Sleep(time.Minute * 5)
    tellEndSmallBreak()
    chanBreak <- true
}

func pomodoroLongBreak(chanLongBreak chan bool) {
    tellBeginLongBreak()
    time.Sleep(time.Minute * 30)
    tellEndLongBreak()
    chanLongBreak <- true
}
    
func tellBeginTurn() {
    exec.Command("say", "Pomodoro round begins").Output()
}

func tellEndTurn() {
    exec.Command("say", "Round ended").Output()
}
    
func tellBeginSmallBreak() {
    exec.Command("say", "Have a small break!").Output()
}

func tellEndSmallBreak() {
    exec.Command("say", "This is the end of the small break. Let's go back to work!").Output()
}

func tellBeginLongBreak() {
    exec.Command("say", "Have a long break! You deserved it!").Output()
}

func tellEndLongBreak() {
    exec.Command("say", "This is the end of the long break. Let's go back to work!").Output()
}

A goroutine for a pomodoro turn just tells something with the say command and then sleeps for a given amount of time using the time.Sleep call. When the goroutine is waken up, we again tell that it is the end with say and send a message on the channel to inform the always running service that a turn just ended. Because we have nothing left to do, we go out of the function and the goroutine closes as well. Goroutines for the small and the long break are behaving the same way.


Now let's see the code for the always running pomodoro service which will launch other goroutines and inform the main thread.

func pomodoroService(chanPomodoro, chanBreak, chanLongBreak, chanDone chan bool) {
    fmt.Println("Pomodoro service started\n")
    for {
        select {

        case endTurn := <-chanPomodoro:
            _ = endTurn
        if currentTurn >= totalTurns {
            go pomodoroLongBreak(chanLongBreak)
            currentTurn = 1
        } else {
            currentTurn += 1
            go pomodoroBreak(chanBreak)
        }

        case endSmallBreak := <-chanBreak:
            _ = endSmallBreak
            go pomodoroTurn(chanPomodoro)

        case endLongBreak := <-chanLongBreak:
            _ = endLongBreak
            input := askAnotherSession()
            for input != "Y" && input != "N" {
                input = askAnotherSession()
            }
            if input == "Y" {
                go pomodoroTurn(chanPomodoro)
            } else {
                chanDone <- true
            }

        }
    }
}
    
func askAnotherSession() string {
    fmt.Println("Ready for another pomodoro session? (Y/N)")
    var input string
    fmt.Scanln(&input)
    return input
}

This goroutine executes an infinite loop using a for construct. Then inside this for we use select to actually switch between messages received over a channel. If we receive a message from chanPomodoro then we launch another goroutine for a short break or a long break depending on the current number of elapsed turns. If we receive a message from chanBreak, we launch another pomodoro with a new goroutine. Finally if we receive a message from chanLongBreak, we ask the user for another round of pomodoro and if the answer is No, we inform the main thread that we can close the program by sending a message on the done channel.

Program Structure

Let's recap with a schema showing the overall program structure:

| Program begins (main implicit goroutine)
|
| Initializing Go Channels
|   - turn
|   - break
|   - longBreak
|   - done
|
| go pomodoroTurn(turn) > |
|                         | 
|                         | Sleep(25 minutes)
|                         |
| go pomodoroService(turn, break, longBreak, done) > |
|                         |                          |
|                         |           done           |
|                         |  ----------------------> |
|                         | end                      | case endTurn
|                                                    | currentTurn < totalTurns
|                                                    | go pomodoroBreak(chanBreak) > |
|                                                    |                               | Sleep(5 minutes)
|                                                    |              done             |
|                                                    |  <--------------------------  |
|                                                    |                               | end
|                                                    | case endBreak
|                                                    | go pomodoroTurn(chanPomodoro) > |
|                                                    |                                 | Sleep(25 minutes)
|                                                    |              done               |
|                                                    |  <----------------------------  |
|                                                    |                                 | end
|                                                    | case endTurn
|                                                    | currentTurn < totalTurns
|                                                    |
|                                                    |

[...]                                              [...]
// Go on until the end of the fifth pomodoro turn

|                                                    | case endTurn
|                                                    | currentTurn >= totalTurns
|                                                    | go pomodoroLongBreak(chanLongBreak) > |
|                                                    |                                       | Sleep(30 minutes)
|                                                    |                 done                  |
|                                                    |  <----------------------------------  |
|                                                    |                                       | end
|                                                    | caseLongBreak
|                                                    | Another Session? NO!
|                        done                        |
|  <-----------------------------------------------  |
|
| Message received from the Pomodoro Service: We are done!
|
| End Program

Conclusion

In this post, we covered the basic usage of goroutines and go channels in Go by programming a simple pomodoro tool. You can find a slightly evolved version of this program with simple task management features on Github.

Have fun with Go and goroutines!

channelsconcurrencyconcurrentProgramminggoroutinegolanggo

Comments


Related Posts

Members Public

Reviving the libkv library

In 2015, two months before the release of docker 1.9 which included container networking, we had a need for a distributed metadata storage solution. The inner working of libnetwork required informations to be accessible to docker engines in a distributed fashion, in order to discover and manipulate libnetwork objects