DI GoLang: From Basics to Advanced Topics

Author

Reads 463

Colorful lines of code on a computer screen showcasing programming and technology focus.
Credit: pexels.com, Colorful lines of code on a computer screen showcasing programming and technology focus.

GoLang is a statically typed language that compiles directly to machine code. This means that GoLang programs run quickly and efficiently.

GoLang was designed to be a modern language that makes it easy to build scalable and concurrent systems. It achieves this by providing a high-level language that abstracts away many low-level details.

GoLang has a simple syntax that is easy to read and write. Its concise syntax makes it a great choice for building small to large-scale applications.

GoLang's concurrency features are a key part of its design. It provides built-in support for goroutines, which are lightweight threads that can run concurrently with the main program.

What Is Injection?

Dependency Injection is a design pattern that helps you decouple the external logic of your implementation. It's common for an implementation to need external dependencies like APIs or databases, and it's not its responsibility to know these things.

Dependency Injection is about receiving dependencies and using them as needed, rather than creating them yourself. This makes it easier to change the behavior of external services or databases.

For your interest: S Golang

Credit: youtube.com, Learning Golang from Zero | Episode #7: Dependency Injection in Go

You should inject abstractions, not implementations, to follow the Dependency Inversion Principle. This allows you to switch easily between different implementations of a dependency.

Injecting implementations would make it hard to change the real implementation for a mock implementation, which is fundamental for unit testing.

Retrieving objects from their definitions is faster than retrieving them from their names, but it requires importing the package containing the definitions, which can lead to import cycles.

Types of Injection

In Go, there are three main types of dependency injection: Constructor, Property, and Method injection.

Constructor injection is the most common kind, making your implementation immutable by requiring all dependencies to be ready to create something. This usually generates an error if they aren’t.

Property and Method injection are similar, and their adoption depends on language features. In Java, Method Injection is more common, while in C#, Property Injection is more common. In Go, both are used.

These two types of injection allow you to change dependencies in runtime, making them not immutable. However, if you need to change the implementation of a dependency, you can simply override what you need without recreating everything.

Take a look at this: Golang Go

Kinds of Injection

Medic Give Injection in Forehead
Credit: pexels.com, Medic Give Injection in Forehead

Constructor Injection is the most common kind of dependency injection. It allows you to make your implementation immutable, nothing can change the dependencies.

This means that all dependencies must be ready to create something, and if they aren’t, it usually will generate an error.

Property and Method injection are pretty similar, I think their adoption is a question of a language feature. In Java, Method Injection is more common, while in C#, Property Injection is more common.

In Go, you'll see the usage of both, allowing you to change dependencies in runtime, making them not immutable.

Injection Container

An injection container is a crucial component in managing dependencies in software development. It's responsible for building the dependency tree and creating instances of structures.

Libraries like dig and wire from Uber and Google respectively, automate dependency injection and make it easier to manage dependencies. Dig, for example, is used to create a container that gathers information on how to create a specific component and creates instances of these components for us.

Workplace with modern laptop with program code on screen
Credit: pexels.com, Workplace with modern laptop with program code on screen

The container is created using the Provide() method, which allows us to add constructors to the container. This can be done using declared or anonymous functions. We can then use the Invoke() method to start the created instance of the server.

Wire, on the other hand, uses code generation to wire dependencies automatically. It's a lightweight DI framework for Go that provides accessors for declaring dependencies and generates the code that wires these dependencies together during the build process.

Here are some key features of Wire:

  • Simplicity: Wire is a lightweight and easy-to-use DI framework for Go.
  • Compile-time safety: Wire performs dependency graph analysis at compile time.
  • Integration with Go tooling: Wire integrates well with other Go tools.

However, Wire has some limitations, including limited features and configuration options. It focuses primarily on initialization-based DI and lacks advanced features like scoping, middleware, and interceptors.

To reuse the container, we can extract the container building into a separate function that we can call from all entry points in our application. This allows us to write a test that ensures the tree is built correctly and prevents tree-building errors at runtime.

How to Use

Credit: youtube.com, #41 Golang - Master Dependency Injection in Go

To use di golang, start by creating a definition with a Build function to create the object. This function is essential for defining the object's behavior.

A definition can be added to a builder with the Add method, which allows you to combine multiple definitions into a single container. You can add as many definitions as you need to create a complex object.

Once all the definitions are added, you can call the Build method to generate a container. This is the point where the magic happens, and your object starts to take shape. The container will then store the objects for later use.

How to Do It

To add a definition, use the Add method to add it to a builder. This is the first step in creating a container.

You can then call the Build method on the builder to generate a container. This is where the magic happens and your definitions come to life.

Close-up of colorful programming code displayed on a computer screen.
Credit: pexels.com, Close-up of colorful programming code displayed on a computer screen.

The Get method returns an interface{} that you need to cast before using the object. This is a crucial step in accessing the objects in your container.

Use a di.Def as the parameter to the Get method for the fastest results. If you don't have a di.Def, using the name of the definition is still a good choice.

The first time you call the Get method, the definition's Build function is called to create the object. After that, the same object is returned unless the definition has its Unshared field set to true.

This means that multiple calls to the Get method will return the same object, which can be a good thing if you're trying to be efficient.

Definition Build

A definition only requires a Build function to create an object. This function returns the object and an error if it can't be created.

Panic in the Build function is recovered and works as if an error was returned. This means you can use a try-catch block to handle any errors that might occur.

Intriguing read: Golang Build

Programming Language on a Screen
Credit: pexels.com, Programming Language on a Screen

The Build function returns an object that can be used immediately, but it will only be called the first time the Get method is called. After that, the same object is returned.

You can use the Build function to create objects that depend on other objects defined in the container. This allows you to build complex objects that have relationships with other objects.

Frameworks and Tools

In Go, you have several options for implementing Dependency Injection (DI), and choosing the right framework is crucial for a smooth development process.

You should evaluate the frameworks and select one that best fits your specific project requirements and development preferences.

Project complexity is a significant factor to consider when choosing a DI framework, as some frameworks are better suited for large-scale applications.

When selecting a DI framework, it's essential to consider the desired level of customization, as some frameworks offer more flexibility than others.

Community support is also vital, as it can impact the availability of resources and documentation for your project.

Explore further: Golang Web Programming

Container Management

Credit: youtube.com, Use Docker for Your Golang Projects with Live Reloading

Creating a container is the first step in using a dependency injection library like Dig. This container gathers information on how to create specific components and creates instances of them for us.

To provide the container with information about our components, we add their constructors to the container using the Provide() method. We can pass either declared or anonymous functions to this method.

Initializing the container is as simple as calling the InitializeContainer function, which is a straightforward step in setting up our dependency injection system.

Builder

If you add two definitions with the same name, the first one is replaced.

You need to handle errors properly, even if it's not the case in this example for conciseness.

A definition only requires a Build function to create the object it represents.

This Build function returns the object and an error if it can't be created.

Panic in a Build function is recovered and works as if an error was returned.

You can't use the same definition in two different EnhancedBuilder instances.

Updating a definition once it's been added to the builder is also not possible.

Consider reading: T Golang

Initialize Container

Architect holding a blueprint and phone, smiling in a library workspace.
Credit: pexels.com, Architect holding a blueprint and phone, smiling in a library workspace.

Initializing the container is a crucial step in container management. The InitializeContainer function is responsible for this process.

To start, you need to gather information on how to create specific components, which is done by adding their constructors to the container using the Provide() method. This is what the dig library from Uber does, building the dependency tree and creating instances of structures.

The container will then create instances of these components for you, making it easier to manage dependencies between them. This is especially useful when you have a complex system with many interconnected components.

When you're done registering your components, you can start using the container to create instances of them. This is done using the Invoke() method, which will create all the necessary instances and pass them as arguments to your application.

By initializing the container correctly, you can ensure that your application starts successfully with a correctly built dependency tree. This is especially important when you're dealing with a large and complex system.

Container Deletion

Colorful Hapag-Lloyd shipping containers stacked at a container terminal in Hamburg.
Credit: pexels.com, Colorful Hapag-Lloyd shipping containers stacked at a container terminal in Hamburg.

Deleting a container is a crucial part of container management. You can delete a container when you no longer need it.

Delete closes all the objects stored in the container by calling their Close function. This ensures that resources are released properly.

Deleting a container makes it unusable, but it frees its memory. This is why you should delete containers even if none of your definitions have a Close function.

If there are dependencies between definitions, the Close functions are called in the right order, which is important for maintaining data integrity.

There are two delete methods: Delete and DeleteWithSubContainers. DeleteWithSubContainers deletes the children of the container and then the container, right away.

Delete is a softer approach that doesn't delete the children of the container. Instead, it waits for the last child to be deleted before deleting the parent container.

You should use Delete and close the children manually to avoid potential errors. DeleteWithSubContainers can cause issues if the parent is deleted while its children are still in use.

Don't use unscope functions inside a Build function, as this can lead to circular definitions and infinite loops.

Bean Management

Woman in focus working on software development remotely on laptop indoors.
Credit: pexels.com, Woman in focus working on software development remotely on laptop indoors.

Bean Management is all about registering and managing your beans. The `RegisterBean` function is used to register a bean by type, and it requires a reference type as its beanType parameter.

To specify the scope of a bean, you need to use a tag `di.scope` in a struct. If no scope is explicitly specified, the default scope is `Singleton`. The `RegisterBean` function returns true if a bean with the same ID has already been registered.

You can also use the `RegisterBeanFactory` function to register a bean that's created using a factory. This function requires a bean factory and a bean scope, and it returns true if a bean with the same ID has already been registered. The scope of a bean created with a factory can be any of the supported scopes.

The `RegisterBeanInstance` function is used to register a pre-created instance of a bean. This function always uses the `Singleton` scope, and it returns true if a bean with the same ID has already been registered. The instance parameter must be a reference or an interface.

For more insights, see: Golang Reference

GoIoC

Credit: youtube.com, Dependency Injection Best Practices with the Go Context package

GoIoC is a dependency injection framework for Go that provides a simple and efficient way to manage beans. It allows you to register beans by type, scope, and bean factory.

To register a bean, you can use the RegisterBean function, which takes a bean type, scope, and a bean ID as arguments. The scope of the bean should be defined in a struct using a tag `di.scope` (`Singleton` is used if no scope is explicitly specified).

The RegisterBean function returns a boolean indicating whether the bean with the same ID has been registered already. You can also use the RegisterBeanFactory function to register a bean provided by a bean factory, which can only produce a reference or an interface.

Another way to register a bean is by using the RegisterBeanInstance function, which takes a pre-created instance of the bean and its scope. The scope of such beans is always `Singleton`.

Once you've registered a bean, you can retrieve its type using the GetBeanTypes function, which returns a map of beans registered in the container, omitting bean factories.

For another approach, see: Golang Use Cases

GetInstanceSafe

Futuristic workspace featuring a glowing computer screen with coding displayed, ideal for technology and programming concepts.
Credit: pexels.com, Futuristic workspace featuring a glowing computer screen with coding displayed, ideal for technology and programming concepts.

GetInstanceSafe is a function that returns a bean instance by its ID without panicking.

It's a safer alternative to GetInstance, which may panic if an error occurs.

GetInstanceSafe returns the error instead of panicking, making it a more controlled way to retrieve a bean instance.

This is especially useful when you prefer to handle errors explicitly.

To use GetInstanceSafe, you can call it with the ID of the bean you want to retrieve.

This function will return the instance or an error if something goes wrong.

Scopes and Dependencies

Scopes in di GoLang allow you to get objects from their definitions, which is significantly faster than retrieving objects from their names. This is because retrieving objects from their definitions doesn't require an additional lookup in a map[string]int.

In practice, containers can have the same scope, and objects are built and stored in containers that have the same scope. They are only created when they are requested.

The App container can only get the App object, while a Request container or a SubRequest container can get either the App object or the Request object, possibly by using their parent. This is because of how scopes are defined in di GoLang.

Scopes in Practice

Close-up of a computer screen displaying colorful programming code with depth of field.
Credit: pexels.com, Close-up of a computer screen displaying colorful programming code with depth of field.

In a container, objects are built and stored in containers that have the same scope.

The scope of a container determines which objects can be retrieved from it. For example, an App container can only get the App object.

A Request container or a SubRequest container can get either the App object or the Request object, possibly by using their parent.

Objects are only created when they are requested, which means they are lazy-loaded.

This approach can improve performance by reducing memory usage and only creating objects when needed.

Definition Dependencies

Definition dependencies can be used in the Build function to create objects that depend on other objects defined in the container.

You can't create a cycle in the definitions, where an object needs another object that in turn needs the first object, or an error will be returned.

Retrieving an object from its name instead of its definition requires an additional lookup in a map, making it significantly slower.

You should retrieve objects from their definitions if performance is critical.

Importing the package containing the definitions can lead to import cycles depending on your project structure.

Get Instance

Credit: youtube.com, Q05 Scope

Get Instance is a crucial function when working with scopes and dependencies. It returns a bean instance by its ID, but be aware that it may panic, so use it with caution.

To minimize the risk of panic, consider using GetInstanceSafe instead, which is a safer alternative.

Advanced Topics

In Go, you can use goroutines to run multiple functions concurrently, allowing your program to perform multiple tasks at the same time. This is achieved using the `go` keyword before a function call.

Go's concurrency model is based on the concept of goroutines, which are lightweight threads that can be created and managed easily. Goroutines are scheduled by the Go runtime, which ensures efficient use of system resources.

By using goroutines, you can write asynchronous code that is easier to read and maintain. For example, you can use goroutines to perform time-consuming operations, such as database queries or file I/O, without blocking the main program flow.

Why Go?

Credit: youtube.com, Advanced Golang: Channels, Context and Interfaces Explained

I've been using Dependency Injection in Java for nearly 10 years via Spring Framework.

Go follows a different ideology than Java, but that doesn't mean it doesn't need Dependency Injection.

You may argue that Go's better world doesn't need DI, but it's proven to be very useful for large enterprise-level applications.

I decided to create a light-weight Spring-like library for Go, and you're free to not use it.

Go's ideology values different principles and paradigms, but that doesn't mean DI isn't needed.

One can live without DI, but it's worth considering for large-scale projects.

DI is not just for Java, it's a useful tool for many programming languages.

Deep Nesting

Deep nesting can make your code slower to build. Definitions with a lot of dependencies at several levels are likely to be slow to build compared to creating the object manually.

This is because each dependency adds a layer of complexity, making it harder for your code to resolve. Deep nesting can lead to a long chain of dependencies, which can cause performance issues.

In some cases, deep nesting might be unavoidable, but it's worth exploring alternative designs to minimize dependencies and make your code more efficient.

Performance and Optimization

Credit: youtube.com, Golang Performance Hack That Reduces GC Overhead

Retrieving an object from a container will always be slower than directly using a variable. That being said, DI tries to minimize the cost of using containers.

In practice, this means that while using Dependency Injection (DI) might not be the fastest approach, it's still a viable option for managing dependencies.

The cost of using containers is minimized by DI, making it a reasonable choice for many use cases.

This is especially true when working with complex systems where managing dependencies can be a challenge.

DI helps to reduce the overhead of using containers, making it a more efficient approach than directly using variables.

Examples and Usage

To get started with DI in Golang, you can use the sarulabs/di-example repository as a reference. This repository demonstrates how to use dependency injection in a web application.

A simple example without HTTP helpers can be found in the article section, but it's worth noting that real-world applications will likely require error handling.

To create a container, you need to add definitions to a builder using the Add method, and then call the Build method to generate the container.

Basic Usage

Woman Using Canon Dslr Camera
Credit: pexels.com, Woman Using Canon Dslr Camera

To start building with a definition, you need to include a Build function to create the object. This function is essential for generating the object.

A definition can be added to a builder with the Add method, which allows you to combine multiple definitions into a single container. This method is a crucial step in the building process.

Once all the definitions are added to the builder, you can call the Build method to generate a container. This method is what brings all the definitions together.

The Get method returns an interface{} that you need to cast to use the object. This method is used to retrieve objects from the container.

The container will only call the definition's Build function the first time the Get method is called. This means that subsequent calls to Get will return the same object, unless the definition has its Unshared field set to true.

Dig

Dig is a powerful toolkit for dependency injection (DI) developed for Go by Uber. It uses reflection to perform runtime DI and provides a flexible API for defining and resolving dependencies.

Credit: youtube.com, Dig Command - How to use it?

Dig's flexibility is one of its standout features, allowing for control over the injection process. This is especially useful for complex projects that require advanced configuration and flexibility.

Dig also allows for custom container configurations, making it possible to define scopes, handle lifecycles, and configure other advanced DI settings.

One of the benefits of using Dig is its ability to detect potential errors or misconfigurations in dependency graphs during code compilation. This can save developers a lot of time and effort in the long run.

Dig requires manual configuration to provide the dependencies to the Dig container, which can be time-consuming, especially for large projects with complex dependency graphs. This may be a drawback for some developers.

Http Helpers

HTTP helpers are a powerful tool in our toolkit. The HTTPMiddleware function can be used to inject a container in an http.Request.

For each http.Request, a sub-container of the app container is created. It is deleted at the end of the http request. This ensures that resources are cleaned up after each request.

Take a look at this: Golang Net/http

People Working in the Office
Credit: pexels.com, People Working in the Office

The container can be used in the handler. Don't forget to use another middleware to recover from a panic and log the errors, as the handler and middleware can panic.

This is crucial because it allows us to catch and handle unexpected errors, making our application more robust and reliable.

Examples

The sarulabs/di-example repository is a great example of how Dependency Injection (DI) can be used in a web application. This repository is a good place to start if you want to learn more about DI in a real-world setting.

One example of a web application that uses DI is the one found in the sarulabs/di-example repository. You can also check out the blog post "How to write a REST API in Go with DI" for more information.

Here is a shorter example of a web application that uses DI, but does not handle errors:

  • It does not use the HTTP helpers.

Middleware and V1.3.0

Middleware in Go is a powerful tool that allows you to inject request-scoped beans into the web request context.

Credit: youtube.com, How Golang Middleware Works + Some Middleware You're Gonna Want in Your API

With the introduction of Middleware in version 1.1.0, you can now use functions with http routers to perform bean injection. This feature is particularly useful for handling request-scoped beans.

If a bean implements io.Closer, it will automatically be closed upon context cancellation, preventing resource leaks.

Middleware is a simple yet effective way to manage request-scoped beans, making it easier to write clean and efficient code.

Frequently Asked Questions

Does Uber still use Golang?

Yes, Uber still uses Golang to support its large-scale microservices architecture. With over 2,000 microservices and 46 million lines of Go code, Golang remains a crucial part of Uber's technology stack.

Rosemary Boyer

Writer

Rosemary Boyer is a skilled writer with a passion for crafting engaging and informative content. With a focus on technical and educational topics, she has established herself as a reliable voice in the industry. Her writing has been featured in a variety of publications, covering subjects such as CSS Precedence, where she breaks down complex concepts into clear and concise language.

Love What You Read? Stay Updated!

Join our community for insights, tips, and more.