Awesome Go

go-beans

CategoryMiscellaneous
SubcategoryDependency Injection
Stars2

Spring-inspired dependency injection and application lifecycle framework for Go

About go-beans

This large README is shown as a plain-text preview. Read the full README at the source

# go-beans

[![Go Reference](https://pkg.go.dev/badge/github.com/go-beans/go.svg)](https://pkg.go.dev/github.com/go-beans/go)
[![Release](https://img.shields.io/github/v/release/go-beans/go)](https://github.com/go-beans/go/releases)

Lightweight, inversion of control implementation inspired by Spring, made for Go idioms

## Using the Container

The `ApplicationContext` is the service for an advanced factory capable of maintaining a registry of beans and their dependencies.  
For container-managed beans, use the `inject` field tag for dependencies:

```go
service type `inject:"optionalName"`
```

For non-container-managed objects:

```go
var service = ioc.Resolve[type]("optionalName")
```

## Bean Overview

An IoC container manages one or more beans. These beans are registered using the form that you supply to the container.

```go
ioc.Bean[type]().
    Scope("scope").
    Name("name").
    Profile("profileExpression").
    ConditionalOnProperty("property", "optionalValue").
    ConditionalOnExpression("expression").
    ConditionalOnBean(type, "optionalName").
    ConditionalOnMissingBean(type, "optionalName").
    Primary().
    Lazy().
    DependsOn("name").
    Order(order).
    Phase(phase).
    Factory(method).
    PostConstruct(method).
    PreDestroy(method).
    EventListener(method).
    Register()
```

All configuration methods are optional except `Factory(method)` and the `Register()`.

Within the container itself, these bean definitions are represented as `BeanDefinition` objects, which contain (among other information) the following metadata:

- A type of the bean being defined.
- Bean behavioral configuration elements, which state how the bean should behave in the container (scope, lifecycle callbacks, and so forth).

This metadata translates to a set of properties that make up each bean definition.

## Instantiating Beans

A bean definition is essentially a recipe for creating one or more objects. The container looks at the recipe for a named bean when asked and uses the configuration metadata encapsulated by that bean definition to create (or acquire) an actual object. `type` can be pointer to a struct `*Service` or interface `MyInterface`. `*Service` implements (can be assigned to) `MyInterface` if methods have a pointer receiver. You can provide `factory` as a reference to `func NewService() *Service` or inline implementation.

Dedicate a file a.k.a. _context_ or _configuration_ for bean definitions inside package `init` method. One can reuse configured and ready-to-go services in different parts of application by for importing package where needed and injecting beans.

project/internal/package/context/context.go:

```go
func init() {
  ioc.Bean[*package.ServiceA]().Factory(package.NewServiceA).
    PostConstruct((*package.ServiceA).PostConstruct).Register()

  ioc.Bean[*package.ServiceB]().Factory(package.NewServiceB).Register()

  ioc.Bean[*cron.Cron]().Factory(func() *cron.Cron { return cron.New() }).
    PostConstruct((*cron.Cron).Start).
    PreDestroy(func(c *cron.Cron) { <-c.Stop().Done() }).Register()

  ioc.Bean[*pgxpool.Pool]().Factory(func() *pgxpool.Pool {
    url := env.Value[string]("postgres://${db.user}:${db.password}@${db.addr}/${db.name}")
    return optional.OfCommaErr(pgxpool.New(context.Background(), url)).OrElsePanic("Unable to create connection pool")
  }).PreDestroy((*pgxpool.Pool).Close).Register()

  ioc.Bean[*http.Client]().Factory(func() *http.Client {
    return env.ConfigurationProperties("package.httpClient", env.ConfigurationProperties("httpClient", &http.Client{
      Transport: env.ConfigurationProperties("package.httpClient.transport", env.ConfigurationProperties("httpClient.transport", &http.Transport{}))}))
  }).Register()

  ioc.Bean[*redis.Client]().Factory(func() *redis.Client {
    return redis.NewClient(env.ConfigurationProperties("package.redis", env.ConfigurationProperties("redis", &redis.Options{})))
  }).PreDestroy(func(c *redis.Client) { c.Close() }).Register()

  ioc.Bean[*concurrent.Executor]().Name("publishExecutor").Factory(func() *concurrent.Executor {
    return concurrent.NewExecutor(env.Value[int]("${package.publishParallelism}"), 0)
  }).PreDestroy((*concurrent.Executor).Shutdown).Register()
}
```

## Dependencies

A typical enterprise application does not consist of a single object (or bean). Even the simplest application has a few objects that work together to present what the end-user sees as a coherent application. This next section explains how you go from defining a number of bean definitions that stand alone to a fully realized application where objects collaborate to achieve a goal.

### Dependency Injection

Dependency injection (DI) is a process whereby objects define their dependencies (that is, the other objects with which they work) only through arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse (hence the name, Inversion of Control) of the bean itself controlling the instantiation or location of its dependencies on its own by using direct construction of objects.

Code is cleaner with the DI principle, and decoupling is more effective when objects are provided with their dependencies. The object does not look up its dependencies and does not know the location or type of the dependencies. As a result, your services become easier to test, particularly when the dependencies are on interfaces, which allow for stub or mock implementations to be used in unit tests. Each service knows and cares only about its own dependencies when `Service A` uses `Service B` uses `Service C`.

project/internal/package/ServiceA.go:

```go
type ServiceA struct {
	httpClient      *http.Client         `inject:""`
	redisClient     *redis.Client        `inject:""`
	scheduler       *cron.Cron           `inject:""`
	serviceB        *ServiceB            `inject:""`
	publishExecutor *concurrent.Executor `inject:"publishExecutor"`

	schedule string `value:"${package.schedule:* * 31 2 *}"`
	workDir  string `value:"${app.workDir:.}"`

	cronEntryId cron.EntryID
}

func NewServiceA() *ServiceA {
  return &ServiceA{}
  // return env.ConfigurationProperties("package.serviceA", &ServiceA{})
}

func (this *ServiceA) PostConstruct() {
  err.Assert(os.MkdirAll(this.workDir, 0755), "Cannot create %s", this.workDir)
}

// Implements ioc.Lifecycle
func (this *ServiceA) Start() {
  this.cronEntryId = optional.OfCommaErr(this.scheduler.AddJob(this.schedule, this)).OrElsePanic("Failed to schedule ServiceA")
  slog.Info(fmt.Sprintf("Scheduled ServiceA with id %v, next run at %v", this.cronEntryId, this.scheduler.Entry(this.cronEntryId).Next))
}

// Implements ioc.Lifecycle
func (this *ServiceA) Stop() {
  this.scheduler.Remove(this.cronEntryId)
}

// Implements cron.Job
func (this *ServiceA) Run() {
  ...
}
```

project/cmd/package/main.go:

```go
package main

import _ "project/internal/package/context"

func main() {
  defer ioc.Close()
  ioc.Run()
  // ioc.AwaitTermination()
}
```

## Bean Scopes

When you create a bean definition, you create a recipe for creating actual instances of the class defined by that bean definition. The idea that a bean definition is a recipe is important, because it means that, as with a type, you can create many object instances from a single recipe.

You can control not only the various dependencies and configuration values that are to be plugged into an object that is created from a particular bean definition but also control the scope of the objects created from a particular bean definition. This approach is powerful and flexible, because you can choose the scope of the objects you create through configuration instead of having to bake in the scope of an object at the type level. Beans can be defined to be deployed in one of a number of scopes.

| Scope     | Description                                                                              |
| --------- | ---------------------------------------------------------------------------------------- |
| singleton | (Default) Scopes a single bean definition to a single object instance for IoC container. |
| prototype | Scopes a single bean definition to any number of object instances.                       |

### The Singleton Scope

Only one shared instance of a singleton bean is managed, and all requests for beans with a name that match that bean definition result in that one specific bean instance being returned by the container.

To put it another way, when you define a bean definition and it is scoped as a singleton, the IoC container creates exactly one instance of the object defined by that bean definition. This single instance is stored in a cache of such singleton beans, and all subsequent requests and references for that named bean return the cached object.

### The Prototype Scope

The non-singleton prototype scope of bean deployment results in the creation of a new bean instance every time a request for that specific bean is made. That is, the bean is injected into another bean or you request it through an `ioc.Resolve()` method call on the container. You may want to use the prototype scope for some stateful beans but note that `PreDestroy(method)` is called for `singleton` beans only for `ioc.Close()`.

## Lifecycle Callbacks

`PostConstruct(method)` is invoked after the bean has been instantiated and its dependencies have been injected. Non-lazy singleton beans are initialized during `ioc.Refresh()`. Lazy singleton beans are initialized when first required, including when another bean requests them during refresh. A lazy bean that is instantiated during refresh completes initialization before refresh finishes.

`PreDestroy(method)` is invoked during `ioc.Close()` for singleton beans, allowing them to release resources before the application shuts down.

Initialization callbacks run on the original bean instance, even when a `BeanPostProcessor` replaces the instance exposed by the container.

## Bean Post-Processors

A `BeanPostProcessor` can replace a singleton bean with another instance, such as a caching, monitoring, or security proxy.

Post-processors run before and after bean initialization, either during `ioc.Refresh()` or when a lazy bean is initialized later. They may return an updated instance or a replacement assignable to the bean's registered type.

When multiple post-processors replace the same bean, each processor receives the instance returned by the preceding processor. After `ioc.Refresh()` completes, dependency injection and providers returned by `ioc.Resolve()` expose the final bean replacement.

Only fields marked with `inject` are updated. Proxy delegate references should not use the `inject` tag when they intentionally refer to an earlier instance in the replacement chain.

### Initialization Order

Injecting a bean does not, by itself, establish an initialization dependency. Use `DependsOn` when a bean requires another bean’s initialization to have completed before its own initialization callbacks execute.

Beans are initialized in ascending order of their `Order(order)` value or `Ordered.Order()` result.

Beans with the same order are initialized in registration order. When bean definitions are registered in package `init()` functions, their registration order follows the order in which those functions execute.

`DependsOn("name")` declares an explicit initialization dependency. The container ensures that the named bean completes initialization before executing the dependent bean’s initialization callbacks. Unlike `Order(order)`, which controls the general initialization sequence, `DependsOn` establishes an initialization dependency between specific beans.

A missing named dependency or a circular `DependsOn` declaration causes bean initialization to fail.

`DependsOn` applies to lazy beans as well. When a lazy bean is instantiated after the context has been refreshed, its declared dependencies are resolved and initialized before its own initialization completes. An invalid dependency declaration causes that operation to fail.

Ordinary circular dependency injection is supported. However, circular `DependsOn` declarations cannot satisfy their initialization ordering requirements and therefore cause an error.

Use an explicit order when initialization callbacks must execute in a particular sequence. A bean may receive an injected dependency before that dependency's `PostConstruct` or `AfterPropertiesSet()` callback has executed.

## Application Lifecycle

The `ApplicationContext` manages the complete lifecycle of beans and application events.

### Startup Sequence

```text
0. Bean registration
   Bean definitions are registered in the ApplicationContext.

Refresh phase:

1. Bean instantiation
   Non-lazy singleton beans are created.

2. Aware callbacks
   BeanNameAware, ApplicationContextAware.

3. Configuration and dependency injection
   Bean properties are configured and dependencies are injected.

4. PostProcessBeforeInitialization
   Post-processors may replace bean instances.

5. PostConstruct
   The original bean receives its custom initialization callback.

6. InitializingBean.AfterPropertiesSet()
   The original bean receives its final initialization callback.

7. PostProcessAfterInitialization
   Post-processors may replace bean instances.

8. Lifecycle.Start()
   Lifecycle beans are started by phase.

9. ContextRefreshedEvent
   The context has been refreshed.

Run phase:

10. ApplicationStartedEvent
    Application has started, before runners.

11. ApplicationRunner.Run()
    Application runners are executed by order.

12a. ApplicationReadyEvent
     Application is ready to serve.

12b. ApplicationFailedEvent
     Startup failed.
```

### Shutdown Sequence

```text
13. ContextClosedEvent
    Context shutdown has been requested.

14. Lifecycle.Stop()
    Started lifecycle beans are stopped in reverse phase order.

15. PreDestroy
    Custom pre-destroy callback is invoked.

16. DisposableBean.Destroy()
    Bean receives final destroy callback.
```

## Application Termination

go-beans supports both short-running batch applications and long-running applications.

If `ioc.AwaitTermination()` is not used, the application behaves like a batch job: `ioc.Run()` starts the context, executes application runners, publishes lifecycle events, and then the `main` function returns normally. Graceful shutdown still happens when `ioc.Close()` is deferred. `ioc.Close()` publishes `ContextClosedEvent`, stops `Lifecycle` beans, invokes destroy callbacks, and releases the global `ApplicationContext`.

```go
func main() {
  defer ioc.Close()
  ioc.Run()
}
```

For long-running applications, use `ioc.AwaitTermination()` to keep the main goroutine alive until the process receives a termination signal or the application is closed explicitly through `ioc.Exit`.

```go
func main() {
  defer ioc.Close()
  ioc.Run()
  ioc.AwaitTermination()
}
```

`ioc.Exit` gracefully closes the current ApplicationContext and terminates
the process with the specified exit code. It never returns.  
`ioc.Exit` is intended only for exceptional application-wide termination cases,
for example:

- fatal unrecoverable conditions requiring a specific process exit code
- command-line/batch applications returning operational status codes
- explicit user-triggered application termination (for example GUI "Exit")
- controlled process termination initiated by infrastructure components

## Application Events

go-beans provides support for application events and listeners.

### Built-in Application Events

| Event                     | Description                                                                                                                                                                                                                                                                                                                      |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ContextRefreshedEvent`   | Published when the `ApplicationContext` is initialized or refreshed by calling `Refresh()`. At this stage, all non-lazy singleton beans are instantiated, dependencies are injected, initialization callbacks are completed, and `Lifecycle` beans are started. The context is fully initialized and ready for use.              |
| `ApplicationStartedEvent` | Published when the application startup sequence begins after the context has been refreshed and before `ApplicationRunner` beans are executed.                                                                                                                                                                                   |
| `ApplicationReadyEvent`   | Published after all `ApplicationRunner` beans have completed. At this stage, the application is considered ready to serve requests and report readiness.                                                                                                                                                                         |
| `ApplicationFailedEvent`  | Published when application startup fails with an unrecovered error.                                                                                                                                                                                                                                                              |
| `ContextStartedEvent`     | Published when the `ApplicationContext` is explicitly started by calling `Start()`. Here, "started" means that all `Lifecycle` beans receive an explicit start signal. Typically, this event is used when restarting components after a previous `Stop()` call.                                                                  |
| `ContextStoppedEvent`     | Published when the `ApplicationContext` is explicitly stopped by calling `Stop()`. Here, "stopped" means that all started `Lifecycle` beans receive an explicit stop signal. A stopped context may later be restarted through `Start()`.                                                                                         |
| `ContextClosedEvent`      | Published when the `ApplicationContext` is being closed by calling `Close()`. At this stage, the application shutdown sequence begins, `Lifecycle` beans are about to be stopped, and singleton beans are about to be destroyed. Once closed, the context reaches the end of its lifecycle and cannot be refreshed or restarted. |

### Listening for Application Events

Register one method (or more) using `.EventListener(...)`

```go
ioc.Bean[*ServiceA]().
  Factory(NewServiceA).
  EventListener((*ServiceA).OnApplicationReady).
  Register()
```

```go
// Method parameter type will be used to filter eligible events to pass to the consumer
func (this *ServiceA) OnApplicationReady(event *ioc.ApplicationReadyEvent) {
  slog.Info("application ready")
}
```

### Publishing Custom Application Events

Any reference or value type may be used as an event.

```go
type UserCreatedEvent struct {
  UserID int64
}
```

Publisher service needs `ApplicationContext` to publish events

```go
type ServiceB struct {
  ctx *ioc.ApplicationContext
}

// Implements ApplicationContextAware
func (this *ServiceB) SetApplicationContext(ctx *ioc.ApplicationContext) {
  this.ctx = ctx
}

func (this *ServiceB) SomeMethod() {
    this.ctx.PublishEvent(&UserCreatedEvent{UserID: 123})
}
```

Consumer registration

```go
ioc.Bean[*app.ServiceA]().Factory(app.NewServiceA).
  EventListener((*app.ServiceA).OnUserCreated).
  Register()
```

Frequently Asked Questions

What is go-beans?

go-beans is a Miscellaneous library for the Go programming language. Spring-inspired dependency injection and application lifecycle framework for Go

How do I install go-beans?

Install go-beans with the Go module system using `go get go-beans/go`. Check the repository for the current installation instructions.

What category does go-beans belong to?

go-beans is listed under Miscellaneous, specifically Dependency Injection.

← Back to Dependency Injection