The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are notified automatically. Observers register interest in a subject and react to events without the subject knowing anything about their concrete types.
It sits in the Behavioral family because it defines how objects communicate — the subject broadcasts state; observers decide what to do with it.
- Multiple components need to react to events produced by a single source.
- You want to decouple the event producer from its consumers so either side can change independently.
- The number or type of subscribers is not known at compile time and may change at runtime.
Avoid it when the notification fan-out is always to one fixed receiver — direct method calls are simpler and clearer.
Classical OOP defines abstract Subject and Observer base classes. Go replaces both with interfaces and first-class functions:
| Mechanism | Purpose |
|---|---|
Observer interface |
Declares the subscriber contract (Name, Notify) |
Subject interface |
Declares the publisher contract (RegisterObserver, Notify, GetObserverList) |
PriceTracker (concrete subject) |
Maintains the observer registry and fans out price events |
ObservationHook |
A predicate that each observer registers alongside itself, filtering which events it receives |
Concrete observers (RetailInvestor, StopLossAlert, AuditLogger) |
Each encapsulates one reaction to a price event |
observer/
├── observer/
│ ├── observer.go # Observer interface and NotificationSignal
│ └── retail_investor.go # Concrete observers
├── subject/
│ └── subject.go # Subject interface and ObservationHook
├── domain/
│ └── stock_price_tracker.go # Concrete subject (PriceTracker)
└── main.go # Usage demo
The observer interface — observer/observer.go
type Observer interface {
Name() string
Notify(NotificationSignal) error
}Any type that implements Name and Notify can subscribe to a PriceTracker — no embedding or registration outside of the call site.
The subject interface — subject/subject.go
type Subject interface {
Notify(string, float64) error
GetObserverList() []observer.Observer
RegisterObserver(observer.Observer, ObservationHook) error
}Each observer registers with a hook — a predicate func(ticker string, price float64) bool. The subject evaluates the hook before dispatching, so an observer only fires when its condition is met:
// Only notified when TCS drops below ₹2895.50
priceTracker.RegisterObserver(&retail, subject.NewCustomObservationHook(
func(ticker string, amt float64) bool {
return ticker == "TCS" && amt < 2895.50
},
))
// Notified on every price event
priceTracker.RegisterObserver(&auditLogger, subject.NewGenericObservationHook())This is an extension of the classic pattern — instead of all observers receiving every event, each declares its own interest condition at registration time.
Concrete observers — observer/retail_investor.go
| Type | Reaction |
|---|---|
RetailInvestor |
Logs the price event for the investor |
StopLossAlert |
Fires an alert when a price crosses a configured threshold |
AuditLogger |
Records every event unconditionally for audit purposes |
Concrete subject — domain/stock_price_tracker.go
PriceTracker holds observers in a map[Observer]ObservationHook. On Notify, it iterates the map, evaluates each hook, and calls ob.Notify only for satisfied hooks. Duplicate registration is rejected with an error.
priceTracker.Notify("TCS", 2890.00)
→ hook for RetailInvestor satisfied (TCS < 2895.50) → [RetailInvestor] notified
→ hook for StopLossAlert not satisfied (ticker != INFY) → skipped
→ hook for AuditLogger always satisfied → [AuditLogger] notified
priceTracker.Notify("INFY", 1280.00)
→ hook for RetailInvestor not satisfied (ticker != TCS) → skipped
→ hook for StopLossAlert satisfied (INFY < 1300) → [StopLossAlert] notified
→ hook for AuditLogger always satisfied → [AuditLogger] notified
- Define a new struct (e.g.,
DividendAlert). - Implement
Name() stringandNotify(NotificationSignal) error. - Register it with
priceTracker.RegisterObserver(&alert, hook).
No changes to PriceTracker, the Subject interface, or any existing observer — Open/Closed Principle in practice.
Q: How does Observer differ from Strategy? Strategy selects one algorithm and executes it in place of another. Observer broadcasts one event to many independent subscribers. Strategy is about substitution; Observer is about decoupled fan-out.
Q: How does Observer differ from Pub/Sub? In Observer, the subject holds a direct reference to each observer — it's synchronous and in-process. In Pub/Sub, a message broker sits between publisher and subscriber, enabling async delivery, persistence, and cross-process communication. Observer is a simpler, tighter coupling; Pub/Sub is Observer scaled out.
Q: What is the risk of a naive Observer implementation?
Memory leaks — if observers register but never deregister, the subject holds references indefinitely. This implementation uses an explicit map, so removing an observer is straightforward to add via a DeregisterObserver method.
Q: Why use a map instead of a slice for the observer registry? O(1) duplicate detection on registration and O(1) removal — both are O(n) with a slice. The tradeoff is that iteration order is non-deterministic, which is acceptable here since observers are independent.
Q: How would you make PriceTracker thread-safe?
Protect the observers map with a sync.RWMutex — a read lock in Notify (while iterating), a write lock in RegisterObserver. Each observer's Notify could still run concurrently by fanning out into goroutines with a WaitGroup.
Q: Where does ObservationHook sit in classic GoF? It is not part of the original GoF Observer. It is a filter layer added between the subject and observers, similar to the Event Filter or Conditional Observer extension — useful when the subject emits high-frequency events and observers only care about a subset.