A small service locator library with the following features
-
Uses generics to provide a type safe interface without using reflection.
-
Automatically resolves dependency initialization like dig.
-
An "hooks" system to easily specify the order of setup operations in a deterministic order.
Install this package with
$ go get github.com/aziis98/go-sl
// Define each service struct or interface with a corresponding slot
var ConfigSlot = sl.NewSlot[*Config]()
var LoggerSlot = sl.NewSlot[*log.Logger]()
var DatabaseSlot = sl.NewSlot[Database]()
var HandlerSlot = sl.NewSlot[Handler]()
var ServerSlot = sl.NewSlot[*Server]()
...
func main() {
l := sl.New()
sl.ProvideFunc(l, ConfigSlot, func(sl *sl.ServiceLocator) (*Config, error) {
err := godotenv.Load()
...
return &Config{ ... }, nil
})
sl.ProvideFunc(l, LoggerSlot, func(l *sl.ServiceLocator) (*log.Logger, error) {
config := sl.MustUse(l, ConfigSlot)
return log.New(os.Stderr, fmt.Sprintf("[service foo=%s] ", config.Foo), log.Lmsgprefix), nil
})
sl.ProvideFunc(l, ServerSlot, func(l *sl.ServiceLocator) (*Server, error) {
config, err := sl.Use(l, ConfigSlot)
if err != nil {
return nil, err
}
handler, err := sl.Use(l, HandlerSlot)
if err != nil {
return nil, err
}
return &Server{ ... }, nil
})
srv := sl.MustUse(l, ServerSlot)
log.Fatal(srv.ListenAndServe())
}
For example a database
module / service can be defined as follows
package database
type Database interface {
CreatePost(content string) (uint, error)
ReadPost(id uint) (string, error)
ReadAllPosts() ([]string, error)
UpdatePost(id uint, newContent string) error
DeletePost(id uint) error
}
var Slot = sl.NewSlot[Database]()
We can then have various implementations like database/mock.go
package database
type Mock struct {
posts map[uint]string
}
func (db *Mock) CreatePost(content string) (uint, error) { ... }
func (db *Mock) ReadPost(id uint) (string, error) { ... }
func (db *Mock) ReadAllPosts() ([]string, error) { ... }
func (db *Mock) UpdatePost(id uint, newContent string) error { ... }
func (db *Mock) DeletePost(id uint) error { ... }
func NewMockDatabase() *Mock {
return &Mock{ ... }
}
func ConfigureMockDatabase(l *sl.ServiceLocator) (Database, error) {
cfg, err := sl.Use(l, config.Slot)
if err != nil {
return nil, err
}
mock := NewMockDatabase()
...
return mock, nil
}
And then in the main call
func main() {
...
sl.ProvideFunc(l, database.Slot, database.ConfigureMockDatabase)
...
}
func main() {
...
sl.ProvideHook(l, router.ApiSlot,
routes.UseAuthRoutes,
routes.UseUserRoutes,
routes.UseSearchRoutes,
routes.UsePostsRoutes,
)
...
}
package routes
func UseAuthRoutes(l *sl.ServiceLocator, r chi.Router) error {
cfg := sl.MustUse(l, config.Slot)
db := sl.MustUse(l, database.Slot)
r.Post("/register", ...)
r.Post("/login", ...)
return nil
}
[...]
[...]
[...]
[...]