# gookit/event, a small event bus for Go with priority and wildcards

> gookit/event is a Go event manager and dispatcher that supports multiple listeners per event, listener priority, and wildcard name matching, with a path mode that treats dots as path separators. Its public API is small enough to read in one sitting.

**gookit/event** — 📢 Lightweight event manager and dispatcher implements by Go. Go实现的轻量级的事件管理、调度程序库, 支持设置监听器的优先级, 支持使用通配符来进行一组事件的监听

- Repository: https://github.com/gookit/event
- Website: https://pgk.go.dev/github.com/gookit/event
- Stars: 580 · Forks: 69
- Language: Go
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/gookit-event

## An event bus that fits on one screen

gookit/event is described as a lightweight event management and dispatch tool library implemented in Go, with a Chinese description alongside it that says the same thing: a Go implementation of a lightweight event management and scheduling library, supporting setting listener priority and using wildcards to listen for a group of events.

The scope is narrow and the code reflects it. The top-level entries of the repository are a `manager.go`, a `manager_fire.go`, a `manager_test.go`, an `event.go`, an `event_test.go`, a `types.go`, a `std.go`, a `util.go` and a `simpleevent/` directory, plus a `benchmark_test.go` and an `issues_test.go`. That is a small library, not a framework, and the public surface is listed in the documentation under a heading called main method.

The methods are the whole API. `On` or `Listen` registers an event listener by name, with an optional priority. `Subscribe` or `AddSubscriber` registers a subscriber, which is the way to attach several listeners at once. `Trigger` or `Fire` fires an event by name with a map of parameters and returns an error and the event. `FireCtx` does the same with a context. `MustTrigger` or `MustFire` fires and panics on error, which is the variant the quick start uses. `FireEvent` fires a specific event instance rather than a name, and `FireBatch` fires several events at once.

The dependency footprint is one direct dependency, `github.com/gookit/goutil` at v0.8.0, plus the standard library. The module requires Go 1.19.

The audience is a Go developer who needs decoupled notification inside one process. The topics list names the concepts it covers: event dispatcher, event listener, event management, eventbus, events and multiple listeners, which is the vocabulary of an in-process bus rather than a broker.

The licence is MIT, the last push to the repository was on 2026-10-19, and the recorded releases are v1.2.0 from 2025-08-30, v1.1.2 from 2024-02-06 and v1.1.1 from 2023-06-10.

## Priority decides who hears an event first

The first feature in the list, after custom event objects and multiple listeners, is listener priority, and the rule is stated simply: the higher the priority, the first to trigger.

The quick start demonstrates it by registering two listeners on the same event name.

```go
package main

import (
	"fmt"
	
	"github.com/gookit/event"
)

func main() {
	// Register event listener
	event.On("evt1", event.ListenerFunc(func(e event.Event) error {
		fmt.Printf("handle event: %s\n", e.Name())
		return nil
	}), event.Normal)

	// Register multiple listeners
	event.On("evt1", event.ListenerFunc(func(e event.Event) error {
		fmt.Printf("handle event: %s\n", e.Name())
		return nil
	}), event.High)

	// ... ...

	// Trigger event
	// Note: The second listener has a higher priority, so it will be executed first.
	event.MustFire("evt1", event.M{"arg0": "val0", "arg1": "val1"})
}
```

The named constants `event.Normal` and `event.High` are priorities, and the note under the example repeats the point: the second listener has a higher priority, so it will be executed first.

The listener signature is a function taking an `event.Event` and returning an error, which is why the example wraps it in `event.ListenerFunc`. That error return is the interesting design decision in the whole library. A listener that fails does not have to be swallowed, because the fire method returns an error, and `MustFire` turns that error into a panic. The trade is that a single failing listener can affect the dispatch of the others, and the documentation does not describe whether remaining listeners still run after one returns an error.

If you prefer a type over a closure, the interface is one method.

```go
// Listener interface
type Listener interface {
	Handle(e Event) error
}
```

And the custom event object support in the feature list is what lets a listener set data back on the event, as the struct example does with `e.Set("result", "OK")`.

The events themselves carry parameters. The type used throughout is `event.M`, a map, and it is what `Fire` accepts as the params argument.

## Two wildcard modes that mean different things

The wildcard support is the feature with a sharp edge, and the library has two modes for it.

The default is `ModeSimple`. A listener registered on `app.*` fires for `app.run` and `app.end`, so the prefix and everything under it matches. There is also support for using the wildcard `*` to listen for triggers for all events, which is the catch-all subscription.

The second mode, `ModePath`, is described as a new match mode and a new pattern from v1.1.0, where the wildcard matching logic has been adjusted. In this mode `*` only matches a segment of characters that are not `.`, allowing for finer monitoring and matching, and `**` matches any number of characters and can only be used at the beginning or end.

The example registers four listeners against one manager and fires one event.

```go
em := event.NewManager("test", event.UsePathMode)

// register listener
em.On("app.**", appListener)
em.On("app.db.*", dbListener)
em.On("app.*.create", createListener)
em.On("app.*.update", updateListener)

// ... ...

// fire event
// TIP: will trigger appListener, dbListener, createListener
em.Fire("app.db.create", event.M{"arg0": "val0", "arg1": "val1"})
```

Firing `app.db.create` triggers the `app.**` listener, the `app.db.*` listener and the `app.*.create` listener, and the tip in the comment says exactly that. The `app.*.update` listener does not fire, because the third segment is `create` and not `update`.

The difference between the modes is one character of behaviour and it matters. In simple mode `*` matches across dots, so `app.*` behaves more like a prefix match. In path mode `*` stops at a dot, which is what makes `app.*.create` expressible at all. A subscription written for one mode silently means something else in the other, and the mode is chosen when you construct the manager.

Note also that the manager can be created explicitly, with a name and a mode, as in the example, rather than using the package-level functions. That is the cleaner way to use the library when the wildcard semantics matter, because the mode is visible at the point of construction rather than implied by a default.

## Async firing trades ordering for a channel

The async mode exists for when a listener should not run on the firing goroutine, and it is implemented with a channel.

The methods are `Async` and `FireC` for pushing an event onto the channel, `FireAsync` for pushing an event instance, and `AsyncFire` to fire asynchronously using the `go` keyword. The documentation says the events will be written to chan for asynchronous consumption.

Two options tune the behaviour. `ChannelSize` sets the buffer size for the channel, and `ConsumerNum` sets how many goroutines to start to consume events.

```go
func main() {
	// Note: close event chan on program exit
	defer event.CloseWait()
	// defer event.Close()
	
    // register event listener
    event.On("app.evt1", event.ListenerFunc(func(e event.Event) error {
        fmt.Printf("handle event: %s\n", e.Name())
        return nil
    }), event.Normal)
    
    event.On("app.evt1", event.ListenerFunc(func(e event.Event) error {
        fmt.Printf("handle event: %s\n", e.Name())
        return nil
    }), event.High)
    
    // ... ...
    
    // Asynchronous consumption of events
    event.FireC("app.evt1", event.M{"arg0": "val0", "arg1": "val1"})
}
```

The lifecycle requirement is the part that bites, and the documentation is explicit about it: the event chan should be closed when the program exits, with `event.Close()` closing the chan and no longer accepting new events, and `event.CloseWait()` closing the chan and waiting for all event processing to complete. The `defer event.CloseWait()` at the top of the example is what stops a program from exiting with events still queued.

That is the real cost of the async mode. In a synchronous dispatch, a fire call is done when it returns. In the channel mode, an event is accepted and processed later, and the program has to cooperate at shutdown or the events are lost. The `ConsumerNum` setting also means the ordering guarantee you get in synchronous mode, where priority decides the order, does not obviously survive a pool of consumers.

For a long-running service that fires events on request paths and does not care about ordering, this is a good trade. For a program that must not exit with work outstanding, it is a shutdown requirement you have to remember.

## Where this bus stops, and what you use instead

The boundaries of gookit/event follow from its design, and they are clear.

It is in-process and in-memory. There is no persistence, no delivery guarantee, and no network. A subscriber in another process, or on another machine, is not reachable through this library. If you need events to cross a process boundary, you need something else, and the alternative would be a message broker or a queue library where the queue is the durable transport. The difference in approach is that a broker owns delivery, retries and consumer offsets, while this library owns ordering and matching inside one process and leaves everything else to you.

Within the process, the library is complete for the common case. Multiple listeners, priority, wildcards, batch firing, context-aware firing, sync and async. What it does not have is any form of event persistence, so a listener that panics or a process that dies takes the pending event with it.

The subscriber interface is the extension point for a group of listeners, which is what the documentation calls registering multiple event listeners, and it is the way to attach a cohesive set of handlers to one call rather than registering them one by one.

There is one more constraint that is easy to miss and that the documentation does not dwell on. The `MustFire` family panics if there is an error, which is convenient in a script and dangerous in a server, where a listener error that arrives from a request path becomes a crash. The non-Must variants return the error, and the choice of which to use is a decision the caller has to make consciously.

The honest summary is that this is a well-scoped, small library that does one thing and documents what it does. Its wildcard support, in particular, is more than a comparable minimal event bus usually offers, and the priority mechanism is a real feature rather than an afterthought.

## Conclusion

gookit/event suits a Go developer who needs named events with several independent listeners, priority ordering and a wildcard subscription, without pulling in a larger broker or a full pub-sub library. It does not suit a distributed system, since the dispatch is in-process and in-memory, and it does not suit a project that needs durable delivery, because the async mode hands events to a channel that has to be closed before the program exits. Verify first that the wildcard semantics you need match the mode you choose, since the default simple mode treats a prefix differently from the path mode added in v1.1.0, and confirm that your listeners return errors in a way your code actually handles.

## FAQ

### How do I install gookit/event?

With go get github.com/gookit/event. The module requires Go 1.19 and has a single direct dependency, github.com/gookit/goutil at v0.8.0.

### How does listener priority work in gookit/event?

Priority is an optional argument to On, and the rule is that the higher the priority fires first. The named constants event.Normal and event.High are used, and the documentation notes that a listener registered with a higher priority executes before one with a lower priority on the same event.

### What is the difference between ModeSimple and ModePath in gookit/event?

ModeSimple is the default, where a listener on app.* fires for app.run and app.end. ModePath, added in v1.1.0, changes the matching so that * only matches a segment of characters that are not a dot, while ** matches any number of characters and can only be used at the beginning or end.

### How do I fire events asynchronously with gookit/event?

Use the Async, FireC or FireAsync methods, which push the event onto a channel for asynchronous consumption. You can set the channel buffer size with ChannelSize and the number of consumer goroutines with ConsumerNum, and you must close the channel when the program exits using event.Close() or event.CloseWait(), which waits for all event processing to complete.

### How do I write a listener as a type rather than a closure in gookit/event?

Implement the event.Listener interface, which has a single method Handle(e Event) error. The interface is the extension point, and the documentation notes that a custom event object can be used to set data back on the event from a listener.

## Sources

- [gookit/event on GitHub](https://github.com/gookit/event)
- [License: MIT](https://github.com/gookit/event/blob/master/LICENSE)
- [Project website](https://pgk.go.dev/github.com/gookit/event)
- [README](https://github.com/gookit/event/blob/master/README.md)
- [Releases](https://github.com/gookit/event/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gookit-event
