Hooks provides a simple, type-safe hook system to enable easier modularization of your Go code. A hook allows various parts of your codebase to tap into events and operations happening elsewhere which prevents direct coupling between the producer and the consumers/listeners.
For example, a user package/module in your code may dispatch a hook when a user is created, allowing your notification package to send the user an email, and a history package to record the activity without the user module having to call these components directly. A hook can also be used to allow other modules to alter and extend data before it is processed.
Hooks can be very beneficial especially in a monolithic application both for overall organization as well as in preparation for the splitting of modules into separate synchronous or asynchronous services.
This is based on https://github.com/mikestefanello/hooks and extended heartily.
- Start by declaring a new hook which requires specifying the type of data that it will dispatch as well as a name. This can be done in a number of different way such as a global variable or exported field on a struct:
package user
type User struct {
ID int
Name string
Email string
Password string
}
var HookUserInsert = hooks.NewHook[User]("user.insert")
- Register for a hook:
package greeter
func init() {
user.HookUserInsert.Register(func(e hooks.Event[user.User]) {
sendEmail(e.Msg.Email)
})
}
- Dispatch the data to the hook Registrants:
func (u *User) Insert() {
db.Insert("INSERT INTO users ...")
HookUserInsert.Dispatch(context.Background(), &u)
}
Hooks are dispatched, by default, concurrently with a concurrency of 1. That means you can raise the limit and increase the concurrency. You define this when the hook is created.
Using the HookUserInsert
example above:
var HookUserInsert = hooks.NewHook[User]("user.insert").WithLimit(10)
With the limit raised to 10, multiple registrants can concurrently execute. Bounded by a rate of 10 at once. If a registrant finishes before another, it frees capacity and another may start.
This is an important distinction. The work doesn't work in batches. If the concurrency is set to N, we don't wait until all N are complete before starting more work. As each item completes, it frees a token for another to work.
The dispatcher will block until all registrants finish.
The context.Context
argument let's the dispatcher cancel the work or setup a
deadline. This way, should the caller no longer need the work, we're not
burning cycles.
A registrant can Unregister
which means our basic hook/notification approach
can be used as a sort of simple message bus. Let's walk through this with an example.
- Start by declaring a new hook which requires specifying the type of data that it will dispatch as well as a name. This is the same as before.
- Register for a hook and Unregister on some condition:
package greeter
func init() {
user.HookUserInsert.Register(func(e hooks.Event[user.User]) {
if someCondition() {
e.Unregister()
}
sendEmail(e.Msg.Email)
})
}
- Dispatch the data to the hook Registrants: