Skip to content
Francisco Cidade
7 min read

Concurrency and Parallelism in Go - Goroutines

Summary

Goroutines

Gopher

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 vs. 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

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.

  1. Write a program that launches a goroutine responsible for printing the message “Hello, Go!” on the screen.

  2. Write a program that starts a goroutine running an anonymous function.
    That function must print “Running anonymous function in the goroutine”.

  3. 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.).

  4. Write a program that defines a function (for example, called longJob) that:

    • Prints “Starting long job”,
    • Does a time.Sleep of 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.
  5. 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.Sleep between iterations.
    • In main, after starting the goroutines, wait long enough (with time.Sleep) for both to finish.

Common mistakes with goroutines

Ending the program before the goroutines run

Capturing loop variables incorrectly

Not handling parameter passing in the anonymous function

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!

References: