Go Concurrency Patterns A Fun and Practical Guide

Writed by Moncef in 12/06/2024.

Hello, Go enthusiasts! Today, we're diving into the exciting world of Go concurrency patterns. We'll explore three key patterns using fun, relatable examples from our "Go Concurrency Circus" ๐ŸŽช. Let's make parallel processing as entertaining as a three-ring show!

Table of Contents

Introduction

Go's concurrency model is based on Communicating Sequential Processes (CSP). It's like coordinating a circus performance - different acts running simultaneously, communicating and synchronizing to create a spectacular show.

Key components:

  • Goroutines: Our performers
  • Channels: The means by which they communicate
  • Select: The stage manager, deciding which act goes on next

Let's dive into our main acts!

The For-Select Juggling Act

The for-select pattern is like a skilled juggler, handling multiple balls (channels) without dropping any. It's perfect for managing multiple channels non-blocking.

forselectpattern/forselect.go

package forselectpattern

import (
	"fmt"
)

func PerformJugglingAct() {
	juggler := make(chan string, 3)
	tricks := []string{"Cascade", "Shower", "Mills Mess"}

	for _, trick := range tricks {
		select {
		case juggler <- trick:
			fmt.Printf("Juggler starts %s\n", trick)
		default:
			fmt.Printf("Oops! Juggler dropped the %s\n", trick)
		}
	}
	close(juggler)

	for trick := range juggler {
		fmt.Printf("Juggler completes %s. Applause!\n", trick)
	}
}

In this act, our juggler (the for-select loop) attempts to perform three tricks. If the juggler can't start a trick immediately (channel is full), they "drop" it and move on. This demonstrates non-blocking channel operations.

Use this pattern when you need to:

  • Handle multiple channels without blocking
  • Implement timeouts or cancellation
  • Create responsive, event-driven programs

The Done Channel Magic Door

The done channel pattern is like a magician's vanishing act. It provides a clean way to signal that it's time to finish up and exit stage left.

donepattern/done.go

package donepattern

import (
	"fmt"
	"time"
)

func PerformMagicDoorTrick() {
	magicDoor := make(chan bool)

	go func() {
		for {
			select {
			case <-magicDoor:
				fmt.Println("Magician: Abracadabra! The door vanishes!")
				return
			default:
				fmt.Println("Magician: The door is still here...")
				time.Sleep(time.Second)
			}
		}
	}()

	time.Sleep(time.Second * 3)
	close(magicDoor)
	fmt.Println("Audience: Wow! The door disappeared!")
}

In this trick, our magician (goroutine) keeps checking if the magic door (done channel) has vanished. When the audience (main goroutine) decides it's time, they make the door disappear, signaling the magician to finish the act.

Use this pattern when you need to:

  • Gracefully terminate long-running goroutines
  • Implement cancellation in concurrent operations
  • Avoid goroutine leaks

The Pipeline Assembly Line

Pipelines in Go are like an assembly line in a widget factory. Each stage processes the data and passes it down the line.

pipelinepattern/pipeline.go

package pipelinepattern

import "fmt"

func RunAssemblyLine(numbers ...int) {
	if len(numbers) == 0 {
		numbers = []int{1, 2, 3, 4, 5}
	}

	// Stage 1: Parts Maker
	partsMaker := func(numbers []int) <-chan int {
		out := make(chan int)
		go func() {
			for _, n := range numbers {
				out <- n
			}
			close(out)
		}()
		return out
	}

	// Stage 2: Widget Assembler
	widgetAssembler := func(in <-chan int) <-chan int {
		out := make(chan int)
		go func() {
			for n := range in {
				out <- n * n
			}
			close(out)
		}()
		return out
	}

	// Run the assembly line
	parts := partsMaker(numbers)
	widgets := widgetAssembler(parts)

	// Quality Control
	for widget := range widgets {
		fmt.Printf("Quality Control: Widget %d passed inspection!\n", widget)
	}
}

In our widget factory, we have a parts maker (generating numbers), a widget assembler (squaring the numbers), and quality control (printing the results). Each stage operates independently, connected by channels.

Pipelines are perfect for:

  • Processing streams of data
  • Separating concerns in data processing
  • Improving modularity and testability

Putting It All Together

Now, let's see how we can combine all these acts into one grand performance!

main.go

package main

import (
	"fmt"
	"time"

	"github.com/modecode22/concurrency-patterns-experements/donepattern"
	"github.com/modecode22/concurrency-patterns-experements/forselectpattern"
	"github.com/modecode22/concurrency-patterns-experements/pipelinepattern"
)

func main() {
	fmt.Println("Welcome to the Go Concurrency Circus! ๐ŸŽช")

	fmt.Println("\n๐Ÿคน For-Select Juggling Act:")
	forselectpattern.PerformJugglingAct()

	fmt.Println("\n๐Ÿšช Done Channel Magic Door:")
	donepattern.PerformMagicDoorTrick()

	fmt.Println("\n๐Ÿญ Pipeline Assembly Line:")
	pipelinepattern.RunAssemblyLine()

	fmt.Println("\nThat's all, folks! Remember, concurrency is like cooking - timing is everything!")
}

This main function is our ringmaster, introducing each act and ensuring the show runs smoothly.

Conclusion

And there you have it, folks! We've explored the exciting world of Go concurrency patterns through our Go Concurrency Circus. Remember, like in a real circus, coordination and timing are key to a great performance.

Here are some key takeaways:

  1. Use the for-select pattern for juggling multiple tasks without blocking.
  2. The done channel pattern is great for signaling when it's time to pack up and go home.
  3. Pipelines help you break complex processes into manageable, independent stages.

Happy coding, and may your concurrent programs be as entertaining and well-coordinated as a world-class circus act!


For the complete code and more examples, check out our GitHub repository: Go Concurrency Patterns

- Like it ? cost 0$ just share it :)
- About Moncef Aissaoui:

Made selance, rawdati, dushdo. I'm currently building an open-source project for Algerian developers and startup founders named wolfroad and cheapres. I love reading and writing about business and programming.

Get exclusive content