Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
234: Observer Pattern with Channels
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
Subscribemethod that returns a channel for rate updates. - An
Unsubscribemethod that removes the channel from the observer list and closes it to prevent memory leaks. - A
UpdateRatemethod 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).
There are no comments for now.