No history yet

Introduction to Go Concurrency

Handling Multiple Tasks at Once

Many programs do one thing at a time, step-by-step. This is called sequential execution. It’s like a single barista at a coffee shop making one drink from start to finish before even looking at the next order.

But what if you need to handle multiple tasks seemingly at the same time? This is concurrency. In our coffee shop, it’s like having one barista start steaming milk for a latte, and while the milk heats up, they start grinding beans for the next customer's espresso. They are managing multiple tasks by switching between them during moments of waiting. Concurrency is about dealing with lots of things at once.

Go was built from the ground up to make concurrency simple and efficient. This is crucial for modern software, especially for network servers or web services that need to handle thousands of user requests simultaneously.

Go’s powerful concurrency model is one of its standout features.

Meet Goroutines

In Go, a concurrently executing task is called a goroutine. Many other programming languages use something called threads for this purpose. You can think of threads as delivery trucks. They can carry heavy loads (run complex tasks), but they are also slow to start up and consume a lot of resources.

A goroutine is more like a nimble delivery drone. It's incredibly lightweight. You can have thousands, or even millions, of goroutines running at the same time in a single program without breaking a sweat. This makes it easy to structure your program to handle many tasks at once.

Lesson image

Starting a goroutine is surprisingly simple. You just place the go keyword before a function call. This tells Go to run this function concurrently, without waiting for it to finish.

package main

import (
	"fmt"
	"time"
)

func sayHello() {
	fmt.Println("Hello from the goroutine!")
}

func main() {
	// Start a new goroutine
	go sayHello()

	fmt.Println("Hello from the main function!")

	// Wait a moment for the goroutine to run
	time.Sleep(1 * time.Second)
}

In this example, go sayHello() starts a new goroutine. The main function continues its execution immediately and prints its own message. You might notice the time.Sleep() call at the end. Without it, the main function could finish and the entire program would exit before the sayHello goroutine even had a chance to run. This is a clumsy way to wait, and it points to a new problem: how do we coordinate between goroutines?

Passing Messages with Channels

Goroutines need a safe way to communicate and synchronize with each other. In Go, the preferred way to do this is with channels. A guiding principle in Go is: "Do not communicate by sharing memory; instead, share memory by communicating."

Think of a channel as a pneumatic tube connecting two office workers (our goroutines). One worker can put a message in the tube, and the other can safely retrieve it. The tube ensures that messages don't get lost or jumbled, and it provides a way for the workers to coordinate. If a worker tries to take a message from an empty tube, they will simply wait until a message arrives.

channel

noun

A typed conduit that provides a way for goroutines to communicate and synchronize. You can send and receive values using the channel operator, <-.

You create a channel using the make() function. Channels are typed, meaning they can only transport data of a specific type. For example, make(chan string) creates a channel that can only be used to send and receive strings.

Let's update our example to use a channel for communication instead of a time.Sleep().

package main

import "fmt"

// This function accepts a channel to send a message on.
func greet(c chan string) {
	// Send a string into the channel
	c <- "Hello from the goroutine!"
}

func main() {
	// Create a new channel of type string.
	messages := make(chan string)

	// Start a goroutine and pass the channel to it.
	go greet(messages)

	// Wait to receive a message from the channel.
	// This line blocks until a message is received.
	msg := <-messages

	fmt.Println(msg)
}

Here's how it works:

  1. We create a channel called messages that can carry strings.
  2. We launch the greet function as a goroutine, passing the channel to it.
  3. The greet function sends a message into the channel using c <- "...".
  4. Meanwhile, the main function waits to receive a message from the channel with msg := <-messages. This is a blocking operation; main will pause right here until a message is available on the channel.

Once the greet goroutine sends its message, the main function receives it, unblocks, and prints it. This is a much more robust way to coordinate tasks. By using goroutines and channels together, you can build powerful and efficient concurrent programs.

Quiz Questions 1/5

What is the primary characteristic of concurrent execution in programming?

Quiz Questions 2/5

In Go, what is the term for a lightweight, concurrently executing task?