liveBook · Manning

Message passing

As explained in Learn Concurrent Programming with Go

Message passing is a communication approach in which concurrent executions exchange information by sending and receiving messages.

Message passing lets concurrent executions communicate by sending and receiving values rather than directly changing the same memory. In Go, goroutines exchange those values through channels. Each goroutine can keep isolated state, making data flow more explicit and reducing the opportunity for executions to interfere with one another. section 7.0 section 7.1

Think of a group of workers: each completes its own task, then sends a result to another worker or a coordinator. The receiver may act on the message or return a reply. This same model also fits distributed applications, where machines cannot share memory and instead exchange information through protocols such as HTTP. section 7.1

A channel is the Go mechanism for this exchange. An unbuffered channel synchronizes sender and receiver: sending waits for a receiver, and receiving waits for a sender. A buffered channel can hold a fixed number of pending messages, allowing sends to continue until it fills and receives to continue until it empties. Channels can be send-only or receive-only, closed with close(), and consumed until closure with a for range loop. section 7.4

Use message passing to shape concurrent work around clear boundaries:

  • Give each goroutine its own state and exchange result values through input and output channels. section 9.0
  • Send copies of data where possible; sending mutable pointers can quietly reintroduce shared-memory race conditions. section 9.2
  • Collect asynchronous results by receiving from worker output channels and merging them in a coordinator. section 7.4
  • Use select when work involves multiple channels, timeouts, non-blocking operations, or removing closed channels from consideration. section 8.0

Count letters without a shared counter

A worker can download one web page, count letters into its own 26-element slice, and send that slice on a result channel. The main goroutine starts one worker per page, receives each slice, and adds it into a final total. This removes the need for mutexes around one shared frequency slice, but allocates a separate result slice for every page. section 8.2.3

Listing 8.13 Letter-frequency function using message passing (imports omitted)
package main

import (...)

const allLetters = "abcdefghijklmnopqrstuvwxyz"

func countLetters(url string) <-chan []int {
    result := make(chan []int)                    #1
    go func() {
        defer close(result)
        frequency := make([]int, 26)              #2
        resp, _ := http.Get(url)
        defer resp.Body.Close()
        if resp.StatusCode != 200 {
            panic("Server returning error code: " + resp.Status)
        }
        body, _ := io.ReadAll(resp.Body)
        for _, b := range body {
            c := strings.ToLower(string(b))
            cIndex := strings.Index(allLetters, c)
            if cIndex >= 0 {
                frequency[cIndex] += 1            #3
            }
        }
        fmt.Println("Completed:", url)
        result <- frequency                       #4
    }()
    return result
}
#1 Creates output channel of type int slice
#2 Creates a local frequency slice
#3 Updates each character count in the local frequency slice
#4 Once it’s finished, the frequency slice is sent over the channel.

Inspect the worker that creates local frequency state, counts a document, closes its result channel, and sends one completed slice. section 8.2.3

Message passing often produces modules with understandable inputs, outputs, and data flow. Maintaining a channel contract can let a goroutine’s internal logic change with less effect on other modules. It does not automatically guarantee loose coupling, but it makes clear execution boundaries easier to design than shared mutable memory does. section 8.2.1 section 8.2.2

The trade-off is copying and replicated state. Passing large messages or sending very many messages can cost substantial time, while per-goroutine local data can increase memory consumption. Shared memory may be the better choice when large data must be exchanged under tight performance constraints or when a design would otherwise become extremely chatty. section 8.2.3 section 8.2.4

The underlying concurrency model is communicating sequential processes (CSP): sequential executions with isolated state exchange copies of values through channels. Go adapts this model with goroutines and synchronized, unbuffered-by-default channels, while allowing channels themselves to be passed through functions or other channels. section 9.1.2

How Learn Concurrent Programming with Go covers it

How deep it goes
Explains channel blocking, buffering, closure, result collection, selection, CSP, and trade-offs, while deferring deeper CSP detail.
Examples
Uses goroutines that count web-page letter frequencies locally and send slices for aggregation.
What it assumes
Assumes some Go or similar C-style language experience while introducing concurrency communication concepts gently.
What you can do after
Apply it via Go channels to exchange concurrent results, coordinate blocking behavior, and choose against memory sharing when appropriate.
How it connects
Connects channels with goroutines, CSP, memory sharing, race conditions, mutexes, semaphores, and select.

Also explained in

Compare how 9 books explain message passing →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Concurrent Programming with Go. The section links point to the parts of the book it draws on.