The State of Dependency Injections in Golang.

Dependency injection (DI) is a well-established concept in programming. This is the design pattern in software engineering where dependencies are injected into a class rather than the class creating them itself, promoting loose coupling and easier testing.

However, when it comes to Golang, where classic object-oriented programming (OOP) constructs like classes are absent (struct's don’t count, sorry mates), implementing dependency injection poses some slight differences. But you will soon see that this practice is quite easy to implement in Golang, even without third-party dependencies. In fact, I'm 100% certain that you're already using DI in your Golang applications because this pattern is almost unavoidable.

Later in this article, I will demonstrate you how you can enforce your experience with DI in Go by using a simple tool from Google called wire.

DI without dependencies.

If you have ever written unit tests in Go, then you are likely already familiar with this practice. It is the only way to do a unit test when you have some database connections or any kind of third party external service used in your function. You will need to mock this service. To achieve this, you will have to provide it (either to the function directly, or into the struct your method assigned to). You will also need to have an interface for your third-party service and both your mock and your service should implement the methods of an interface.

Accept interfaces, return structs.

For example, you will have a structure like this in some service.go file:

// Some service your App depends on
type ThirdPartyService struct {
    baseURL string
}

func NewThirdPartyService(baseURL string) *ThirdPartyService {
    return &ThirdPartyService{
        baseURL: baseURL,
    }
}

func (t *ThirdPartyService) FindSomething(query string) (string, error) {
    resp, err := http.Get(fmt.Sprintf("%s%s", t.baseURL, query))
    if err != nil {
        return "", err
    }
    defer func(Body io.ReadCloser) {
        _ = Body.Close()
    }(resp.Body)

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    return string(body), nil
}

type ThirdPartyServiceInterface interface {
    FindSomething(query string) (string, error)
}

type App struct {
    service ThirdPartyServiceInterface
}

func NewApp(service ThirdPartyServiceInterface) *App {
    return &App{
        service: service,
    }
}

func (a *App) DoSomething(query string) (string, error) {
    return a.service.FindSomething(query)
}

This is the way how you can create a dependency injections in Golang, and it also allows you to mock your dependencies in the unit tests.

To use this App in your code, you will need to construct all its dependencies first and then proceed with the NewApp.

In order to unit test your App you will need to mock this ThirdPartyService. There are various methods to generate mocks from the interface, but I will not discuss it here and will stick a manual solution with classic testify/mock and testify/assert which are commonly used in many mockery libraries under the hood.

Here is an example of your test with mock:

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type MockThirdPartyService struct {
    mock.Mock
}

func (m *MockThirdPartyService) FindSomething(query string) (string, error) {
    args := m.Called(query)
    return args.String(0), args.Error(1)
}

func TestApp_DoSomething(t *testing.T) {
    mockThirdPartyService := new(MockThirdPartyService)
    app := NewApp(mockThirdPartyService)

    mockThirdPartyService.On("FindSomething", "test").Return("response", nil)

    result, err := app.DoSomething("test")
    assert.NoError(t, err)
    assert.Equal(t, "response", result)
}

What we just did is created a mock for the ThirdPartyService and injected it as a dependency into your app.

See? You are already employing this design pattern even if you weren't consciously aware of it. There is no way you can escape from dependency injection in Go since you can’t stub and spy on your method as easy as it was in Node.js with a sinonjs library, for example.

That was easy, but what is the challenge?

The challenge comes when your app grows bigger. In fact, you are having multiple binaries using dozens of internal and external dependencies, and it becomes harder to organize and reuse your components and to maintain low coupling.

Now, imagine you have a dozen App structures, each represents the struct of its own binary. And each App is having a unique sets of components, although there may be intersections between them. And some components will have their very own dependencies etc.

Well, it is not a big challenge; you can manage this manually, no doubt. However, this approach will require consistency from you and is likely to be error-prone. Additionally, you'll need to monitor unused dependencies in your components and apps. A preferable approach is to enforce this practice with an external tool to mitigate common mistakes.

There are many dependency injection tools on the market, for example: samber/do, or uber/fx. However, they all share one common trait - it is not just about dependency injection, it is more like a DI framework. Lots of features, necessity to learn specific syntax and write your services and components in the prescribed manner. And sometimes those tools can introduce breaking changes. Furthermore, if you intend to share your components with other services, you'll need to use the same DI tool, or alternatively, write a wrapper.

I want to show you the different approach (my favourite among others) - google/wire. It will be much, much simpler and will not force you to stick with some specific syntax. Your components can be shared with others without forcing them to use your tool.

Wire up with Google.

google/wire is an external tool for Golang made by Google to generate code that helps us to connect component using DI. There is no specific syntax of how you should write your components. It can be a structure of any kind, like you already have. As a result, the learning curve is minimal, as you only need to write a wire.go file.

//go:build wireinject

//go:generate go run github.com/google/wire/cmd/wire

package service

import "github.com/google/wire"

func initApp(baseURL string) (*App, error) {
    wire.Build(
        // All providers needed to create the App or other providers that are needed in between
        NewThirdPartyService,
        // NewApp is the function that creates the final App struct
        NewApp,

        // Bind the interface to the implementation so Wire knows which one to use.
        // Accept interfaces, return structs. Remember?
        wire.Bind(new(ThirdPartyServiceInterface), new(*ThirdPartyService)),
    )

    return &App{}, nil
}

Well, that's basically it. You can run “go generate” to produce the new code:

go generate ./...

This will should generate wire_gen.go file with app/component constructor:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package service

// Injectors from wire.go:

func initApp(baseURL string) (*App, error) {
    thirdPartyService := NewThirdPartyService(baseURL)
    app := NewApp(thirdPartyService)
    return app, nil
}

What it does is try to match all dependencies that you provided with their independent components. One dependency can be used in different components at the same time. There are no strict ordering rules for the dependencies to be processed in wire.Build. If you have redundant dependencies, the wire tool will notify you. Similarly, if you're missing dependencies from your build configuration, it will also alert you.

Now, you can simply run initApp(...) in your main () to access the built App. There's no need to manually create an initialization function for each dependency in every app ever again. This becomes very convenient when you have dozens of components within the same app. For instance, let's consider a more elaborate wire build:

wire.Build(
    ProvideLogs,
    ProvideConfig,
    ProvideServer,
    ProvideHandler,
    ProvideEmailService,
    ProvideAuthService,
    ProvideTemporalService,
    ProvideAwsStorage,
    ProvideAwsSns,
    ProvideGrpcClient,
    NewApp,
)

Logs can be used in any components, so as Config and others. Wire will connect all of them, so you don’t have to worry about unreadable main() or init() functions. Remember to add wire.Bind to associate interfaces with implementations for Wire.

Since one component can have multiple dependencies, you can organize them into ProviderSet by using wire.NewSet() function and storing alongside your component:

var ProviderSet = wire.NewSet(
    ProvideConfig,
    ProvideService,
    wire.Bind(new(types.ServiceInterface), new(*Service)),
)

No magic is done here, no special syntax to learn - it will just make your wire.go files look cleaner by grouping all the things in one place:

func initApp(ctx context.Context, cfg *config.Config) (*serverApp, error) {
    wire.Build(
        db.ProviderSet,
        github.ProviderSet,
        jwt.ProviderSet,
        captcha.ProviderSet,
        mailer.ProviderSet,
        server.ProvideServer,
        handler.ProviderSet,

        newServerApp,
    )

    return &serverApp{}, nil
}

You can find this code example of google/wire usage in my go-bloggy repository. In fact, this blog is built with Wire :)

Wrap up the Wire.

In my opinion, Wire is the easiest and simplest dependency injection tool I’ve seen in my life, not only in Golang exclusively. It just gets the job done and nothing else. No learning curve, no special syntax; you can seamlessly add it to your project, without even touching the code of your existing components. This means your code and components remain highly reusable, even in repositories that don't use Wire. Love it 🧡

Some tips and tricks:

  • Use Provide verb instead of New for components initialization. ProvideService, ProvideClient etc.
  • Combine multiple inner provider dependencies into ProviderSet.
  • Create provider.go file in each component to hold all component initializations and ProviderSets in one place.
  • When you use wire.Bind(new(ServiceInterface), new(*Service)) don’t forget about *Service. It should be a pointer in most cases (if your methods are attached like func (s *Service)).
  • If you see the error “no provider found” - double check that component has all declared dependencies in wire.Build. Also double-check the bindings of interfaces with structs and pointers.
  • There are some uncertainties regarding how to manage configuration. The dependency injection pattern often leads to a scenario where each component has its own sources of configuration (with each component reading only its own environment variables). While this approach helps maintain low coupling, I find it cumbersome to have configuration scattered across multiple places and prefer to consolidate it into a single file. Therefore, my ProvideConfig function will accept the core Config as a parameter and retrieve only the necessary configuration values from it. However, I'm still undecided on the best approach here, so I'm open to suggestions.

22 Apr

2024