The Observer Pattern in Go Using Channels

Bashayr Alabdullah
4 min readDec 26, 2023

By action…

I read Head First Design Patterns by Eric Freeman. It explains object-oriented principles and design patterns based on Java and I’m trying to link the patterns with Golang. So, in this post I write about one of the design pattern that mentioned in Chapter#2: observer pattern.

What is the Observer Pattern?

“The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.”

What is a Channel in Go?

Channel is a YouTube home page for a personal account to upload videos, add comments..etc. Just KIDDING 😆.

Go handles the activities of a concurrent program by goroutines and connect between these goroutines by channels . Therefore, the channel is a communication mechanism which enables one goroutine to send values to another goroutine. By using channels, one goroutines can send data to another, wait for a response, or even signal the completion of a task, all while maintaining efficient code execution.

Short story…

Let’s take Medium blogs as an example, and assume you have a list of favorite authors. Without observer, you’d have to manually check each author’s profile daily for new publications 💫. But, with observer, you will receive a notification of any new articles that have been published. In this scenario, the relation is called publisher and subscribers (single writer multiple readers).

In Go, this publisher-subscriber relationship can be implemented using channels. When the Author publisher has a new update, a message is sent through the channel. As a reader subscriber, you are listening on this channel and receive updates as they happen.

Code Example

I will explore two variations of the Observer Pattern in Go: one without concurrency and another with concurrency.

Problem Statement 🔥

We want to create an Author interface representing a subject in the Observer Pattern. This interface needs to manage a list of readers and notify them when a new article is published. The Author interface has the following operations:

  1. AddReader(reader Reader): Creates a new reader to the author.
  2. RemoveReader(reader Reader): Deletes a reader from the author.
  3. NotifyReaders(): Notifies all registered readers about a new article.

Observer Pattern without concurrency

Here is the full implementation:

Output:

> go run main.go
Author published new article: 'Go Concurrency Patterns'
Reader reader1@example.com is notified about article 'Go Concurrency Patterns'
Reader reader2@example.com is notified about article 'Go Concurrency Patterns'

As we can see in the code, Author Struct maintains a slice of Reader, NotifyReaders method iterating over readers and the readers are updated directly within NotifyReaders .

Wait…🤔 what’s wrong? Actually, there is no problem with the non-concurrent Observer Pattern implementation as it currently works. In this implementation, the NotifyReaders() method processes each reader sequentially. However, let's consider a scenario where the author has a substantial number of readers, say over ~100. In such a case, notifying each reader sequentially could lead to significant delays, right?

Moreover, if we need to introduce additional functionalities for some readers beyond the Update method, we might face the need to modify how NotifyReaders() handles these notifications. This could lead to the function becoming complex. To efficiently manage this, especially in a system with a high volume of readers, utilizing concurrency and channels in Go becomes a practical solution. By implementing parallel notification processes, we can notify each reader simultaneously.

Observer Pattern with concurrency

In the below implementation of the Observer Pattern, each reader is notified in a separate goroutine, enabling parallel processing.

The output will be the same as without the concurrency.

Now, Author Struct map of Reader to channel, NotifyReaders method sends messages over channels, the readers are associated with a channel and they’re updated in separate goroutines. The code uses sync.Mutex to protect shared data, which in this case is the map of readers.

The primary goal of the concurrent here is notifying the readers, which executes the Update method for each reader. In this case, it does not need to collect or process any results from these Update calls and there is no need to gather or process any results from these Update calls, which is why sync.WaitGroup is used. Basically, WaitGroup is a great way to wait for a set of concurrent operations to complete.

This my exploration of the Observer Pattern and Channels in Go. While there are many more details that could be discussed, my goal was to keep this blog focused. For those interested in delving deeper into these topics, I highly recommend the article ‘The beauty of concurrent programming in Go’ and ‘Concurrency in Go’ by Katherine Cox-Buday for further reading.

--

--

Bashayr Alabdullah

Salam, I'm Tech Engineer 🚀. I blend technology insights, inspiration, and fun in my feed. #innovation #discovery 🥑🎶🎯🌻