Lesson 8 of 12

Goroutines & Channels

Learn how to run work concurrently with goroutines and pass data between them with channels. Goroutines are cheap, so Go programs often start thousands of them.

Go
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()               // signal completion no matter how we return
	time.Sleep(100 * time.Millisecond)
	fmt.Printf("worker %d finished\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)                 // count BEFORE starting the goroutine
		go worker(i, &wg)         // "go" runs it concurrently
	}

	wg.Wait()                     // block until every Done() has been called
	fmt.Println("all done")

	// Without the WaitGroup, main would exit immediately and kill every
	// goroutine mid-flight. Nothing waits for a goroutine automatically.
}
Go
package main

import "fmt"

func main() {
	// Unbuffered channel: a send blocks until a receiver is ready.
	done := make(chan string)
	go func() { done <- "finished" }()
	fmt.Println(<-done)

	// Buffered channel: sends succeed until the buffer is full.
	jobs := make(chan int, 100)
	results := make(chan int, 100)

	// Worker pool — three goroutines sharing one queue of work
	for w := 1; w <= 3; w++ {
		go func(id int) {
			for j := range jobs {          // ranges until jobs is closed
				results <- j * j
			}
		}(w)
	}

	for i := 1; i <= 9; i++ {
		jobs <- i
	}
	close(jobs)                            // tells the workers no more work is coming

	total := 0
	for i := 0; i < 9; i++ {
		total += <-results
	}
	fmt.Println(total)                     // 285

	// Direction in the signature documents intent and is compiler-checked
	// func produce(out chan<- int)   send-only
	// func consume(in <-chan int)    receive-only
}
Go
package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

// select waits on several channels at once
func race(a, b <-chan string) string {
	select {
	case msg := <-a:
		return "a: " + msg
	case msg := <-b:
		return "b: " + msg
	case <-time.After(2 * time.Second):
		return "timeout"
	}
}

// context is how you cancel work in Go: pass it as the FIRST parameter.
func fetch(ctx context.Context, url string) (string, error) {
	select {
	case <-time.After(500 * time.Millisecond):     // pretend this is a network call
		return "body of " + url, nil
	case <-ctx.Done():
		return "", ctx.Err()                       // context canceled / deadline exceeded
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()                                 // always cancel to release resources

	if _, err := fetch(ctx, "/slow"); errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("gave up:", err)
	}

	// Mutex when a channel would be overkill (a shared counter, a cache)
	// var mu sync.Mutex; mu.Lock(); defer mu.Unlock()
}

Common question

Is a goroutine a thread?

A goroutine is a running concurrently, by the Go runtime across threads it manages. Channels are typed between goroutines: a send waits until a receiver is ready, which both hands over the value and syncs the two. select waits on several channels at once, and context carries cancellation down a chain of calls.

Design so each piece of data belongs to one goroutine and gets passed on rather than shared. Where sharing cannot be avoided, protect it with a mutex. Always know how each goroutine ends, and run your tests with -race, which finds the mistakes code review misses.

Source: Rob Pike: "Do not communicate by sharing ; instead, share memory by communicating."

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • A goroutine runs concurrently, so it must be an operating-system thread.

  • Channels are safe, so a program that uses them can't deadlock.

  • Starting a goroutine guarantees it will finish before the program exits.

Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.