Goroutines

What are goroutines?
Goroutines are functions or methods that can run in parallel.
They are extremely lightweight and are managed by the Go runtime, which means you can start thousands of goroutines with minimal overhead. This is possible because the Go runtime manages goroutine execution efficiently, distributing them across multiple operating system threads as needed.
The main advantage of goroutines is that they make it easy to build highly concurrent and efficient programs. Instead of creating and managing threads by hand, you can simply use goroutines to run tasks in parallel. This makes the code simpler and easier to maintain, and improves the program’s performance on systems with multiple processing cores.
What is the difference between concurrency and parallelism?

Concurrency:
– It is the ability to make progress on several tasks (or activities) over the same timeline.
– In a concurrent program, several tasks can be started and run, taking turns, without necessarily running at the same instant.
– Concurrency is about dealing with many things at once, structuring the program so it can move several tasks forward while waiting for resources, I/O, etc.
Parallelism:
– It is the actual simultaneous execution of multiple tasks on different cores or processors.
– A parallel program runs more than one task at exactly the same moment, using hardware resources (multi-core or multiple processors).
– Parallelism is one way to implement concurrency when you have hardware that allows real simultaneous execution.
In short, while concurrency is a way of structuring a program so it can handle several tasks (even if those tasks are not literally running at the same time), parallelism refers to running those tasks simultaneously, when the hardware and environment allow it.
Okay, so how do I use a goroutine?
To start a goroutine, just put the keyword go before the function call.
Example:
package main
import (
"fmt"
"time"
)
func main() {
// Starts a goroutine that prints a message
go func() {
fmt.Println("Hello, Go!")
}()
// time.Sleep is used to make sure the goroutine runs before the program exits
time.Sleep(100 * time.Millisecond)
}
In this example, the anonymous function runs as a goroutine. Note that we use time.Sleep to give the goroutine time to run, because main may finish before it.
Using anonymous functions and loop variables
When you use an anonymous function inside a loop to launch goroutines, it is important to remember how the loop variables (like the index) are captured.
Common mistake:
for i := 1; i <= 5; i++ {
go func() {
fmt.Println("Goroutine", i)
}()
}
The code above will make all goroutines print the same value of i, which is the final value of the loop (in this case, 6). This happens because the variable i is captured by the anonymous function, and all goroutines share the same reference to i.
Run the code and you will get this result:
Goroutine 6
Goroutine 6
Goroutine 6
Goroutine 6
Goroutine 6
This can make all goroutines print the same value of i (the final value).
How would you solve this problem?
Solution: Pass the variable as a parameter to the anonymous function:
for i := 1; i <= 5; i++ {
go func(n int) {
fmt.Println("Goroutine", n)
}(i)
}
This way each goroutine receives its own value.
Example of a goroutine with a loop and time.Sleep
-
You can use several goroutines to run tasks in parallel, but remember to make sure (with
time.Sleepor synchronization) that the program waits for the goroutines to run. -
Note: Using
time.Sleepis not recommended; the ideal would be to usechannels,wait groupsormutex(depending on the context). We will get to these topics later. -
Example:
package main import ( "fmt" "time" ) func main() { for i := 1; i <= 5; i++ { go func(n int) { fmt.Printf("Running goroutine %d\n", n) }(i) } // Pause to allow the goroutines to run time.Sleep(200 * time.Millisecond) }
Exercises
I usually need practice to understand something new, otherwise I just scan it with my eyes and forget it right after.
If you are like that too, do the exercise list!
If you run into problems, next there is a section on the most common mistakes, and also how I solved the exercises.
-
Write a program that launches a goroutine responsible for printing the message “Hello, Go!” on the screen.
-
Write a program that starts a goroutine running an anonymous function.
That function must print “Running anonymous function in the goroutine”. -
Write a program that, using a loop, starts 5 goroutines.
Each goroutine must print a message identifying its number (for example, “Goroutine 1”, “Goroutine 2”, etc.). -
Write a program that defines a function (for example, called
longJob) that:- Prints “Starting long job”,
- Does a
time.Sleepof 2 seconds to simulate a heavy task, - And then prints “Long job finished”.
- Start this function in a goroutine and, in
main, print “Launching long job” before starting the goroutine.
-
Write a program that launches two goroutines:
- The first goroutine must, in a simple loop (for example, from 1 to 5), print “Number: X” (where X is the current number) and do a short
time.Sleep(for example, 200 milliseconds) between iterations. - The second goroutine must, also in a 5-iteration loop, print “Letter: A, B, C, …” (you can do something simple or a fixed sequence) with a short
time.Sleepbetween iterations. - In
main, after starting the goroutines, wait long enough (withtime.Sleep) for both to finish.
- The first goroutine must, in a simple loop (for example, from 1 to 5), print “Number: X” (where X is the current number) and do a short
Common mistakes with goroutines
Ending the program before the goroutines run
- Mistake: Not using any way (like
time.Sleep) to give the goroutines time to finish their tasks. - Example: Starting a goroutine that prints something, but right after that
mainends without waiting, so the message is never shown.
Capturing loop variables incorrectly
-
Mistake: In loops that start several goroutines, using the iteration variable directly (e.g.
i) inside the goroutine’s function can make all functions use the same final value ofi(because of variable closure). -
Example:
for i := 1; i <= 5; i++ { go func() { fmt.Println("Goroutine", i) }() }- In this case, all goroutines may print the same value of
i, because the variable is shared.
- In this case, all goroutines may print the same value of
Not handling parameter passing in the anonymous function
- Mistake: If you need to pass a value to the goroutine, forgetting to pass it as a function argument can make the reference be captured even after it changes outside.
Exercise solutions
So I wouldn’t have to run each exercise by hand, I used tests and testable examples.
Exercise 1:
// main.go
func Ex1() {
go func() {
fmt.Println("Hello, Go!")
}()
time.Sleep(time.Second)
}
// main_test.go
func ExampleEx1() {
Ex1()
// output: Hello, Go!
}
Exercise 2:
// main.go
func Ex2() {
go func() {
fmt.Println("Running anonymous function in the goroutine")
}()
time.Sleep(time.Second)
}
// main_test.go
func ExampleEx2() {
Ex2()
// output: Running anonymous function in the goroutine
}
Exercise 3:
// main.go
func Ex3() {
for i := range 5 {
go func(i int) {
fmt.Println("Goroutine", i)
}(i)
}
time.Sleep(time.Second)
}
// main_test.go
// I had to write this helper to be able to read what goes to /dev/stdout
// (since the display order is random)
func captureOutput(f func()) string {
orig := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
os.Stdout = orig
w.Close()
out, _ := io.ReadAll(r)
return string(out)
}
func TestEx3(t *testing.T) {
out := captureOutput(Ex3)
expectedOutputs := []string{
"Goroutine 0",
"Goroutine 1",
"Goroutine 2",
"Goroutine 3",
"Goroutine 4",
}
for _, expected := range expectedOutputs {
if !strings.Contains(out, expected) {
t.Fatal("Output not generated: ", expected, " got: ", out)
}
}
}
Exercise 4:
// main.go
func longJob() {
fmt.Println("Starting long job")
time.Sleep(2 * time.Second)
fmt.Println("Long job finished")
}
func Ex4() {
fmt.Println("Launching long job")
go longJob()
time.Sleep(3 * time.Second)
}
// main_test.go
func ExampleEx4() {
Ex4()
// output:
// Launching long job
// Starting long job
// Long job finished
}
Exercise 5:
// main.go
func Ex5() {
alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
go func() {
for i := range 5 {
fmt.Printf("Number: %d\n", i)
time.Sleep(200 * time.Millisecond)
}
}()
go func() {
for i := range 5 {
fmt.Printf("Letter: %s\n", string(alphabet[i]))
time.Sleep(200 * time.Millisecond)
}
}()
time.Sleep(5 * time.Second)
}
// main_test.go
func TestEx5(t *testing.T) {
out := captureOutput(Ex5)
expectedOutputs := []string{
"Number: 0",
"Number: 1",
"Number: 2",
"Number: 3",
"Number: 4",
"Letter: A",
"Letter: B",
"Letter: C",
"Letter: D",
"Letter: E",
}
for _, expected := range expectedOutputs {
if !strings.Contains(out, expected) {
t.Fatal("Output not generated: ", expected, " got: ", out)
}
}
}
Next steps!
Next we will cover Wait Groups! Click here for part 2!