Documentation
¶
Overview ¶
Package events is a typed, in-process event bus: no `any` at the call site, no type assertion in the handler, and the failure modes handled.
Use it to decouple a side effect from the thing that caused it: checkout publishes OrderPlaced, and the audit log, the email sender and the usage meter each subscribe without the checkout code importing any of them.
Three things it does that a channel and a goroutine do not:
- A handler panic is contained. One bad subscriber cannot take down the publisher, which in an HTTP service is the request.
- Async handlers get a DETACHED context. Handing a handler the request context means the side effect is cancelled when the client disconnects — the notification silently never sends.
- Publish waits for the synchronous handlers and returns their errors, so an effect that MUST happen can be made to block the write that caused it.
What it is not: durable. The process dying loses every unhandled event. When the effect must survive a crash, write it to a table in the same transaction as the state change and publish from there.
Example ¶
Subscribers are typed: no `any` at the call site and no assertion in the handler.
package main
import (
"context"
"fmt"
"github.com/IncepTools/go/src/platform/events"
)
// OrderPlaced is published by checkout. Nothing in checkout imports the
// audit log, the mailer or the usage meter.
type OrderPlaced struct {
OrderID string
Total int64
}
func main() {
bus := events.New()
events.On(bus, "orders.audit", events.Sync,
func(_ context.Context, e OrderPlaced) error {
fmt.Println("audit:", e.OrderID)
return nil
})
events.On(bus, "orders.meter", events.Sync,
func(_ context.Context, e OrderPlaced) error {
fmt.Println("meter:", e.Total)
return nil
})
if err := events.Publish(context.Background(), bus, OrderPlaced{
OrderID: "9c1e", Total: 1200,
}); err != nil {
panic(err)
}
}
Output: audit: 9c1e meter: 1200
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func On ¶
On subscribes to events of type T. name identifies the subscriber in logs, which is the difference between "an event handler failed" and a line you can act on.
func Publish ¶
Publish dispatches an event.
It returns the joined errors of the SYNCHRONOUS subscribers only. Async errors are logged by the bus, because by definition nobody is waiting for them.
Example (AsyncFailuresDoNotReachThePublisher) ¶
An async handler's failure never reaches the publisher: the side effect must not fail the request that caused it. The bus logs it instead.
package main
import (
"context"
"fmt"
"github.com/IncepTools/go/src/platform/events"
)
// OrderPlaced is published by checkout. Nothing in checkout imports the
// audit log, the mailer or the usage meter.
type OrderPlaced struct {
OrderID string
Total int64
}
func main() {
bus := events.New()
events.On(bus, "orders.mail", events.Async,
func(context.Context, OrderPlaced) error {
return fmt.Errorf("the mail server refused the connection")
})
err := events.Publish(context.Background(), bus, OrderPlaced{OrderID: "9c1e"})
bus.Wait()
fmt.Println("publisher saw:", err)
}
Output: publisher saw: <nil>
Types ¶
type Bus ¶
type Bus struct {
// contains filtered or unexported fields
}
Bus dispatches events to subscribers. The zero value is usable.
func (*Bus) OnShutdown ¶
OnShutdown implements lifecycle.Shutdowner, so a bus registered as a hook drains itself on the way down.
func (*Bus) Subscribers ¶
Subscribers lists the handler names registered for each event type, for diagnostics.
type Mode ¶
type Mode int
Mode selects how a subscriber runs.
const ( // Async runs the handler in its own goroutine with a detached context. // Publish does not wait for it and never sees its error. // // This is the right default for a side effect: an audit write must not // slow down, or fail, the request that produced it. Async Mode = iota // Sync runs the handler inline. Publish waits and collects the error. // // Use it only when the publisher genuinely must not proceed if the // handler fails — and prefer a direct call in that case, because a // required dependency hidden behind an event bus is harder to follow than // a function call. Sync )