liveBook · Manning

For-select pattern

As explained in Learn Go with Pocket-Sized Projects

The for-select pattern repeatedly waits for channel communication or control signals within a loop.

A for-select loop repeatedly waits for channel communication. Use it when a goroutine must handle work as it arrives but also stop when it receives a control signal. Unlike `for msg := range channel`, the `select` can wait on multiple channels; after a case runs, the loop waits again. If multiple cases are ready, Go chooses one at random. section 9.5.2

Listing 9.26 explore.go: Stop listening to new messages
func (s *Solver) listenToBranches() {
    wg := sync.WaitGroup{}
    defer wg.Wait()

    for {
        select {
        case <-s.quit: #1
            log.Println("the treasure has been found, stopping worker")
            return
        case p := <-s.pathsToExplore: #2
            wg.Add(1)
            go func(p *path) {
                defer wg.Done()

                s.explore(p)
            }(p)
        }
    }
}

A maze solver listens for new paths and launches an exploration goroutine for each one. Receiving on `quit` returns from the listener; a deferred wait keeps it from finishing before its explorers do. section 9.5.2

A `select` can also choose between sending work and receiving a stop signal. Put the send in its own case rather than checking `quit` with a `default` case and then sending: the stop signal could arrive between that check and the send. A `default` case is useful when the loop has other work to do instead of waiting. section 9.5.2

Listing 9.33 animation.go: Implementing registerExploredPixels
// registerExploredPixels registers positions as explored on the image,
// and, if we reach a threshold, adds the frame to the output GIF.
func (s *Solver) registerExploredPixels() {
    const totalExpectedFrames = 30

    explorablePixels := s.countExplorablePixels() #1
    pixelsExplored := 0

    for {
        select {
        case <-s.quit: #2
            return
        case pos := <-s.exploredPixels: #3
            s.maze.Set(pos.X, pos.Y, s.palette.explored) #4
            pixelsExplored++
            if pixelsExplored%(explorablePixels/totalExpectedFrames) == 0 { 
                s.drawCurrentFrameToGIF()
            }
        }
    }
}

The same pattern drives animation: receive an explored pixel, paint it, and periodically add a GIF frame, or return when `quit` is received. Here the loop performs repeated processing, not just repeated goroutine launches. section 9.6.2

The pattern also fits request-processing loops that should exit when a request is canceled. Keep an exit case alongside the communication that advances the work. section 9.7

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Works through receiving, sending, exit cases, and a default-case race; does not develop a general cancellation API.
Examples
Uses a maze branch listener and a GIF pixel-registration loop.
What it assumes
Assumes familiarity with Go loops, channels, and goroutines while explaining how `select` waits on multiple communications.
What you can do after
Apply it to keep processing channel work until a stop signal arrives, including when sending work might block.
How it connects
Connects Go channels, goroutines, `select`, cancellation, and wait groups.

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