DEV Community

Firas M. Darwish
Firas M. Darwish

Posted on

Ore: Advanced Dependency Injection Package for Go

Ore: An Advanced Dependency Injection Package for Go

Ore Documentation Website

GitHub Repository

Go is known for its simplicity and high performance, but when it comes to managing dependencies, developers often face challenges. While Go doesn’t have a built-in DI framework like some other languages, there are plenty of third-party libraries that can help. Ore is one such package, providing a lightweight and efficient solution for dependency injection (DI) in Go applications.

Ore is designed to make DI easier and more efficient without introducing significant performance overhead. Unlike many other DI libraries, Ore leverages Go generics rather than reflection or code generation, which ensures that your application remains fast and type-safe. This makes Ore a very solid choice for developers looking for an efficient, easy-to-use DI solution.

In this article, we’ll walk through Ore’s key features and how they can help you manage your dependencies in Go. We'll also show a few basic code examples to demonstrate how Ore can be used in real-world applications.


Key Features of Ore

1. Generics-Based Dependency Injection

Ore leverages Go generics to register and resolve dependencies. This design choice avoids the performance overhead typically associated with reflection and code generation. By using generics, Ore ensures that dependency resolution is type-safe and efficient, without requiring any runtime inspection of types.

This approach makes Ore a highly performant DI solution, as it avoids the pitfalls of reflection and code generation, which are common in many other DI frameworks.

2. Simple and Flexible Registration

Ore provides multiple ways to register services, giving you flexibility based on the lifetime of the service (e.g., Singleton, Scoped, Transient). Whether you need a single instance, a scoped instance for a specific context, or a transient instance created every time it’s requested, Ore has you covered.

3. Keyed Services

Ore allows you to register and resolve multiple implementations of the same interface using keyed services. This feature is useful when you need to manage multiple versions of a service or need different behaviors based on some condition.

For example, you could have multiple implementations of a service for different environments (e.g., testing, production) or for different configurations (e.g., based on user roles).

4. Placeholder Services

Ore also supports placeholder services, which allow you to register services with unresolved dependencies that can be filled at runtime. This feature is helpful when certain values or services are not available at the time of registration but can be injected later when they become available.

For instance, you can register a service that requires configuration values, and then provide the actual configuration dynamically based on the context (e.g., user role or environment).

5. Validation

Ore includes built-in registration validation to catch common issues like:

  • Missing dependencies: Ensures all required services are registered.
  • Circular dependencies: Detects and prevents circular dependency chains.
  • Lifetime misalignment: Ensures services with longer lifetimes don’t depend on services with shorter lifetimes.

This validation happens automatically when you resolve services using ore.Get or ore.GetList, but you can also trigger validation manually using ore.Validate(). This ensures that your dependency graph is correct and avoids runtime errors due to misconfigurations.

Additionally, you can disable validation for performance reasons or seal the container to prevent further modifications once all services are registered.

6. High Performance

Performance is a key consideration in Ore. By avoiding reflection and code generation, Ore remains fast, even in large applications with complex dependency graphs. Ore's benchmark results demonstrate its efficiency, with some operations completing in just a few nanoseconds. This makes Ore an excellent choice for high-performance Go applications that need efficient DI without the overhead.

7. Modular and Scoped Containers

Ore provides support for modular containers, allowing you to define separate containers for different parts of your application. This is particularly useful for modular applications, where different components or modules have distinct dependencies. You can define scoped containers for different use cases, making your dependency management more organized and easier to maintain.


Code Examples

To get a better idea of how Ore works, let’s look at a couple of simple examples using the default Ore container.

Example 1: Basic Service Registration and Resolution

package main

import (
    "context"
    "fmt"
    "github.com/firasdarwish/ore"
)

// Define an interface
type Greeter interface {
    Greet() string
}

// Define a service implementation
type FriendlyGreeter struct{}

func (g *FriendlyGreeter) Greet() string {
    return "Hello, world!"
}

func main() {
    // Register the service with the default Ore container
    ore.RegisterFunc[Greeter](ore.Singleton, func(ctx context.Context) (Greeter, context.Context) {
        return &FriendlyGreeter{}, ctx
    })

    // Resolve the service from the default Ore container
    greeter, _ := ore.Get[Greeter](context.Background())
    fmt.Println(greeter.Greet()) // Output: Hello, world!
}
Enter fullscreen mode Exit fullscreen mode

This example demonstrates the basic registration of a service. Here, we define a Greeter interface with a FriendlyGreeter implementation, register it as a singleton, and then resolve it using the default Ore container.

Example 2: Keyed Services for Multiple Implementations

package main

import (
    "context"
    "fmt"
    "github.com/firasdarwish/ore"
)

// Define an interface
type Greeter interface {
    Greet() string
}

// Define multiple implementations
type FriendlyGreeter struct{}

func (g *FriendlyGreeter) Greet() string {
    return "Hello, friend!"
}

type FormalGreeter struct{}

func (g *FormalGreeter) Greet() string {
    return "Good day, Sir/Madam."
}

func main() {
    // Register multiple implementations with keys
    ore.RegisterKeyedFunc[Greeter](ore.Singleton, func(ctx context.Context) (Greeter, context.Context) {
        return &FriendlyGreeter{}, ctx
    }, "friendly")

    ore.RegisterKeyedFunc[Greeter](ore.Singleton, func(ctx context.Context) (Greeter, context.Context) {
        return &FormalGreeter{}, ctx
    }, "formal")

    // Resolve a specific implementation based on the key
    greeter, _ := ore.GetKeyed[Greeter](context.Background(), "friendly")
    fmt.Println(greeter.Greet()) // Output: Hello, friend!

    greeter, _ = ore.GetKeyed[Greeter](context.Background(), "formal")
    fmt.Println(greeter.Greet()) // Output: Good day, Sir/Madam.
}
Enter fullscreen mode Exit fullscreen mode

In this example, we register two implementations of the Greeter interface using keys ("friendly" and "formal") and resolve them based on the required key. This flexibility allows you to manage different implementations easily.


Conclusion

Ore provides a clean, simple, and efficient dependency injection solution for Go. With its use of Go generics, Ore delivers fast and type-safe dependency resolution without the performance overhead of reflection. It’s flexible, easy to use, and includes features like keyed services, placeholder services, and validation to ensure your application remains robust.

Ore Documentation Website

GitHub Repository

Top comments (0)