https://github.com/mikestefanello/hooks
Simple, type-safe hook system to enable easier modularization of your Go code.
https://github.com/mikestefanello/hooks
decoupling generics go golang hook hooks lifecycle-hooks module type-safe typesafe
Last synced: 2 months ago
JSON representation
Simple, type-safe hook system to enable easier modularization of your Go code.
- Host: GitHub
- URL: https://github.com/mikestefanello/hooks
- Owner: mikestefanello
- License: mit
- Created: 2022-09-04T12:48:31.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2022-09-14T21:30:24.000Z (about 3 years ago)
- Last Synced: 2025-05-28T23:04:11.588Z (4 months ago)
- Topics: decoupling, generics, go, golang, hook, hooks, lifecycle-hooks, module, type-safe, typesafe
- Language: Go
- Homepage:
- Size: 31.3 KB
- Stars: 96
- Watchers: 3
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Hooks
[](https://goreportcard.com/report/github.com/mikestefanello/hooks)
[](https://github.com/mikestefanello/hooks/actions/workflows/test.yml)
[](https://opensource.org/licenses/MIT)
[](https://pkg.go.dev/github.com/mikestefanello/hooks)
[](https://go.dev)## Overview
_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.
## Installation
`go get github.com/mikestefanello/hooks`
## Usage
1) 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_:
```go
package usertype User struct {
ID int
Name string
Email string
Password string
}var HookUserInsert = hooks.NewHook[User]("user.insert")
```2) Listen to a hook:
```go
package greeterfunc init() {
user.HookUserInsert.Listen(func(e hooks.Event[user.User]) {
sendEmail(e.Msg.Email)
})
}
```3) Dispatch the data to the hook _listeners_:
```go
func (u *User) Insert() {
db.Insert("INSERT INTO users ...")HookUserInsert.Dispatch(&u)
}
```Or, dispatch all listeners asynchronously with `HookUserInsert.DispatchAsync(u)`.
### Things to know
- The `Listen()` callback does not have to be an anonymous function. You can also do:
```go
package greeterfunc init() {
user.HookUserInsert.Listen(onUserInsert)
}func onUserInsert(e hooks.Event[user.User]) {
sendEmail(e.Msg.Email)
}
```- If you are using `init()` to register your hook listeners and your package isn't being imported elsewhere, you need to import it in order for that to be executed. You can simply include something like `import _ "myapp/greeter"` in your `main` package.
- The `hooks.Event[T]` parameter contains the data that was passed in at `Event.Msg` and the hook at `Event.Hook`. Having the hook available in the listener means you can use a single listener for multiple hooks, ie:```go
HookOne.Listen(listener)
HookTwo.Listen(listener)func listener(e hooks.Event[SomeType]) {
switch e.Hook {
case HookOne:
case HookTwo:
}
}
```- If the `Msg` is provided as a _pointer_, a hook can modify the the data which can be useful to allow for modifications prior to saving a user, for example.
- You do not have to use `init()` to listen to hooks. For example, another pattern for this example could be:```go
package greetertype Greeter struct {
emailClient email.Client
}func NewGreeter(client email.Client) *Greeter {
g := &Greeter{emailClient: client}
user.HookUserInsert.Listen(func (e hooks.Event[user.User]) {
g.sendEmail(e.Msg.Email)
})
return g
}
```- Following the previous example, hooks can be provided as part of exported _structs_ rather than just global variables, for example:
```go
package greetertype Greeter struct {
HookSendEmail *hooks.Hook[Email]
emailClient email.Client
}func NewGreeter(client email.Client) *Greeter {
g := &Greeter{emailClient: client}user.HookUserInsert.Listen(func (e hooks.Event[user.User]) {
g.sendEmail(e.Msg.Email)
})return g
}func (g *Greeter) sendEmail(email string) error {
e := Email{To: email}
if err := g.emailClient.Send(e); err != nil {
return err
}g.HookSendEmail.Dispatch(e)
}
```## More examples
While event-driven usage as shown above is the most common use-case of hooks, they can also be used to extend functionality and logic or the process in which components are built. Here are some more examples.
### Router construction
If you're building a web service, it could be useful to separate the registration of each of your module's endpoints. Using [Echo](https://github.com/labstack/echo) as an example:
```go
package mainimport (
"github.com/labstack/echo/v4"
"github.com/myapp/router"
// Modules
_ "github.com/myapp/modules/todo"
_ "github.com/myapp/modules/user"
)func main() {
e := echo.New()
router.BuildRouter(e)
e.Start("localhost:9000")
}
``````go
package routerimport (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mikestefanello/hooks"
)var HookBuildRouter = hooks.NewHook[echo.Echo]("router.build")
func BuildRouter(e *echo.Echo) {
e.Use(
middleware.RequestID(),
middleware.Logger(),
)
e.GET("/", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "hello world")
})
// Allow all modules to build on the router
HookBuildRouter.Dispatch(e)
}
``````go
package todoimport (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/hooks"
"github.com/myapp/router"
)func init() {
router.HookBuildRouter.Listen(func(e hooks.Event[echo.Echo]) {
e.Msg.GET("/todo", todoHandler.Index)
e.Msg.GET("/todo/:todo", todoHandler.Get)
e.Msg.POST("/todo", todoHandler.Post)
})
}
```### Dependency creation (and injection)
Rather than inititalize all of your dependencies in a single place, hooks can be used to distribute these tasks to the providing packages and great dependency injection libraries like _[do](https://github.com/samber/do)_ can be used to manage them.
```go
package mainimport (
"github.com/mikestefanello/hooks"
"github.com/samber/do""example/services/app"
"example/services/web"
)func main() {
i := app.Boot()server := do.MustInvoke[*web.Web](i)
server.Start()
}
```
```go
package appimport (
"github.com/mikestefanello/hooks"
"github.com/samber/do"
)var HookBoot = hooks.NewHook[*do.Injector]("boot")
func Boot() *do.Injector {
injector := do.New()
HookBoot.Dispatch(injector)
return injector
}
``````go
package webimport (
"net/http""github.com/mikestefanello/hooks"
"github.com/samber/do""example/services/app"
)type (
Web interface {
Start() error
}web struct {}
)func init() {
app.HookBoot.Listen(func(e hooks.Event[*do.Injector]) {
do.Provide(e.Msg, NewWeb)
})
}func NewWeb(i *do.Injector) (Web, error) {
return &web{}, nil
}func (w *web) Start() error {
return http.ListenAndServe(":8080", nil)
}
```### Modifications
Hook listeners can be used to make modifications to data prior to some operation being executed if the _message_ is provided as a pointer. For example, using the `User` from above:
```go
var HookUserPreInsert = hooks.NewHook[*User]("user.pre_insert")func (u *User) Insert() {
// Let other modules make any required changes prior to inserting
HookUserPreInsert.Dispatch(u)
db.Insert("INSERT INTO users ...")
// Notify other modules of the inserted user
HookUserInsert.Dispatch(*u)
}
``````go
HookUserPreInsert.Listen(func(e hooks.Event[*user.User]) {
// Change the user's name
e.Msg.Name = fmt.Sprintf("%s-changed", e.Msg.Name)
})
```### Validation
Hook listeners can also provide validation or other similar input on data that is being acted on. For example, using the `User` again.
```go
type UserValidation struct {
User User
Errors *[]error
}var HookUserValidate = hooks.NewHook[UserValidation]("user.validate")
func (u *User) Validate() []error {
errs := make([]error, 0)
uv := UserValidation{
User: *u,
Errors: &errs,
}if u.Email == "" {
uv.Errors = append(uv.Errors, errors.New("missing email"))
}
// Let other modules validate
HookUserValidate.Dispatch(uv)
return uv.Errors
}
``````go
HookUserValidate.Listen(func(e hooks.Event[user.UserValidate]) {
if len(e.Msg.User.Password) < 10 {
e.Msg.Errors = append(e.Msg.Errors, errors.New("password too short"))
}
})
```### Full application example
For a full application example see [hooks-example](https://github.com/mikestefanello/hooks-example). This aims to provide a modular monolithic architectural approach to a Go application using _hooks_ and [do](https://github.com/samber/do) _(dependency injection)_.
## Logging
By default, nothing will be logged, but you have the option to specify a _logger_ in order to have insight into what is happening within the hooks. Pass a function in to `SetLogger()`, for example:
```go
hooks.SetLogger(func(format string, args ...any) {
log.Printf(format, args...)
})
``````
2022/09/07 13:42:19 hook created: user.update
2022/09/07 13:42:19 registered listener with hook: user.update
2022/09/07 13:42:19 registered listener with hook: user.update
2022/09/07 13:42:19 registered listener with hook: user.update
2022/09/07 13:42:19 dispatching hook user.update to 3 listeners (async: false)
2022/09/07 13:42:19 dispatch to hook user.update complete
```