Skip to Content
Course content

234: Observer Pattern with Channels

Click on the "Edit" button in the top corner of the screen to edit your slide content.

The Observer pattern is one of those classic Gang of Four concepts that feels a bit heavy in languages like Java because of all the interfaces and boilerplate. But in Go, we can implement this much more elegantly using channels. Instead of observers implementing a specific Update() method, they can just listen on a channel. It decouples the "subject" from the "observer" in a way that feels native to the language.

I want to show you this by building a simple stock price monitor. We'll have one PriceTicker that broadcasts price updates to anyone who cares to listen.

Defining the Price Ticker

First, we need a way to manage our observers. I'll use a slice of channels. Each channel will represent a "subscription" to the price updates. I'm using a map here because it makes removing a specific observer much easier later on if we need to.

type PriceTicker struct {
    symbol    string
    observers map[chan float64]bool
    mu        sync.Mutex
}

func NewPriceTicker(symbol string) *PriceTicker {
    return &PriceTicker{
        symbol:    symbol,
        observers: make(map[chan float64]bool),
    }
}

func (pt *PriceTicker) Subscribe() chan float64 {
    pt.mu.Lock()
    defer pt.mu.Unlock()
    
    ch := make(chan float64)
    pt.observers[ch] = true
    return ch
}

Broadcasting the Updates

Now, we need a way to push the new price to everyone. The logic seems simple: loop through the map and send the value. I'll write a Notify method that handles the broadcasting.

func (pt *PriceTicker) Notify(price float64) {
    pt.mu.Lock()
    defer pt.mu.Unlock()

    for ch := range pt.observers {
        ch <- price
    }
}

The Blocking Trap

I've made a mistake here. I've done this plenty of times in the past. Look closely at ch <- price. Because these are unbuffered channels, the Notify method will block until the observer actually reads the value. If one of our observers is doing something slow—like writing to a database or making a network call—the entire PriceTicker stops dead in its tracks. No other observers get their update until the slow one finishes.

In a real-world system, you can't let one slow client freeze your entire data pipeline. That's a recipe for a production outage.

Making Notifications Non-Blocking

To fix this, we have a few options. We could use buffered channels, but that only delays the problem. The more robust way in Go is to wrap the send in a goroutine or use a select block with a default case to drop the message if the observer is too slow. For a stock ticker, usually, the "most recent" price is what matters, so if an observer can't keep up, we can just skip that specific update for them.

Let's update the Notify method to be non-blocking:

func (pt *PriceTicker) Notify(price float64) {
    pt.mu.Lock()
    defer pt.mu.Unlock()

    for ch := range pt.observers {
        // Use a select to avoid blocking the whole loop
        select {
        case ch <- price:
            // Message sent successfully
        default:
            // Observer is lagging; we skip them to keep the system moving
            fmt.Println("Observer lagging, skipping update")
        }
    }
}

Now, the PriceTicker remains responsive regardless of how slow the observers are. I personally prefer this "drop-if-full" approach for telemetry or price feeds; it's better to miss a tick than to crash the whole system.

Wiring it all together

To see this in action, we can spin up a few different "observers"—maybe one that logs to the console and another that triggers an alert if the price hits a certain threshold.

func main() {
    ticker := NewPriceTicker("GOOGL")

    // Observer 1: A simple logger
    logChan := ticker.Subscribe()
    go func() {
        for price := range logChan {
            fmt.Printf("Log: Price updated to %.2f\n", price)
        }
    }()

    // Observer 2: An alert system
    alertChan := ticker.Subscribe()
    go func() {
        for price := range alertChan {
            if price > 150.0 {
                fmt.Printf("ALERT: Price hit %.2f! Sell now!\n", price)
            }
        }
    }()

    // Simulate price movements
    ticker.Notify(145.0)
    ticker.Notify(148.5)
    ticker.Notify(152.1) // This should trigger the alert
}



📋 Practical Task

Build a Multi-Currency Exchange Alert System

Using the concepts from this lesson, build a CurrencyExchange subject that tracks the exchange rate between two currencies (e.g., USD to EUR). Your implementation must include:

  • A Subscribe method that returns a channel for rate updates.
  • An Unsubscribe method that removes the channel from the observer list and closes it to prevent memory leaks.
  • A UpdateRate method that notifies all observers using a non-blocking send.
  • At least two distinct observer goroutines:
    • One that prints every rate change to the console.
    • One that only prints a message when the rate crosses a specific "target" threshold (e.g., alert me when 1 USD > 0.95 EUR).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.