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!
abronan Newsletter
Join the newsletter to receive the latest updates in your inbox.