Golang Dependency Injection based on generics (samber/do, without code generation)
Introduction
The Go programming language, known for its simplicity and efficiency, has introduced generics in its 1.18 release, which can significantly reduce the need for extensive code generation, making the language even more powerful and flexible. For a comprehensive understanding, the Go Generics Tutorial provides an excellent resource.
By using Go’s generics, the samber/do library offers a robust solution for Dependency Injection (DI). DI is a crucial design pattern that fosters loose coupling between objects and their dependencies, thereby promoting code modularity, testability, and maintainability. This combination of generics and DI further elevates Go’s potential in creating efficient, scalable software. In this article, you will learn how to use samber/do to provide dependency injection.
Code Structure
$ tree
.
├── cmd
│ └── web
│ └── main.go # This is the entry point of the application.
├── domain
│ └── user.go # This file defines the business logic of the application.
├── go.mod
├── go.sum
└── user # This directory contains the handler, repository, and service for user-related operations.
├── handler.go
├── repository.go
└── service.go
We use the same example as this story, but by using samber/do lib to implement DI rather than Google Wire. As we can see, the structure of the code becomes much simpler. You can find the source code here.
Object relationship
The domain/user.go defined the business logic structures and interface, as shown below.
type (
User struct {
ID string `json:"id"`
Username string `json:"username"`
}
UserEntity struct {
ID string
Username string
Password string
}
UserRepository interface {
FetchByUsername(ctx context.Context, username string) (*UserEntity, error)
}
UserService interface {
FetchByUsername(ctx context.Context, username string) (*User, error)
}
UserHandler interface {
FetchByUsername() http.HandlerFunc
}
)
You can see the implementation of these interfaces in the /user directory. The relationship can be shown as
UserHandler -> UserService -> UserRepository -> sql.DB
This means that the UserHandler depends on the UserService, which in turn depends on the UserRepository, and finally, the UserRepository depends on sql.DB for database operations. These dependencies are inverted by using interfaces.
It’s a pretty simple example. Let’s provide the objects and wire their depencies now!
cmd/web/main.go
package main
import (
"database/sql"
"example/domain"
"example/user"
"fmt"
"net/http"
_ "github.com/lib/pq"
"github.com/samber/do"
)
func main() {
injector := do.New() // 1
connStr := "user=root dbname=mydb"
db, err := sql.Open("postgres", connStr) // 2
if err != nil {
panic(err)
}
defer db.Close()
do.ProvideNamed[*sql.DB](injector, "user", func(i *do.Injector) (*sql.DB, error) {
return db, nil
}) // 3
do.Provide(injector, user.NewRepository)
do.Provide(injector, user.NewService)
do.Provide(injector, user.NewHandler) // 4
userHandler := do.MustInvoke[domain.UserHandler](injector) // 5
http.Handle("/user", userHandler.FetchByUsername())
fmt.Printf("Try run server at :%d\n", 8080)
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("Error: %v", err)
}
}
Don’t worry, let’s go through the code step by step.
- The main function begins by creating a new DI container with injector := do.New(). This container will be used to manage the dependencies of the application.
- It sets up a connection string and opens a connection to a PostgreSQL database using the sql.Open function.
- It uses the do.ProvideNamed function to add the database connection to the DI container. This function takes three arguments: the DI container, a name for the dependency, and a provider function that returns the dependency and an error. In this case, the dependency is the database connection, and the function simply returns the connection and nil for the error.
- It uses the do.Provide function to add the repository, service, and handler to the DI container. This function takes two arguments: the DI container and a function that returns the dependency and an error. In this case, the functions are user.NewRepository, user.NewService, and user.NewHandler, which create instances of the repository, service, and handler, respectively. Be care that the return type of provider function should be interface, rather than the concrete type. The golang’s pattern “Accept interfaces, return structs” will be supported in v2 !
- It uses the do.MustInvoke function to retrieve the user handler from the DI container and register it with the http package. This function takes two arguments: the DI container and the type of the dependency to retrieve. In this case, it retrieves the user handler and registers its FetchByUsername method as a handler for the /user route.
user/repository.go
package user
import (
"context"
"database/sql"
"example/domain"
"github.com/samber/do"
)
type repository struct {
db *sql.DB
}
func (r *repository) FetchByUsername(ctx context.Context, username string) (*domain.UserEntity, error) {
// use db here
}
// the return type of NewRepository should be interface, rather than the concrete type!
func NewRepository(i *do.Injector) (domain.UserRepository, error) {
db := do.MustInvokeNamed[*sql.DB](i, "user")
return &repository{db: db}, nil
}
user/service.go
package user
import (
"context"
"example/domain"
"github.com/samber/do"
)
type service struct {
repo domain.UserRepository
}
func (s *service) FetchByUsername(ctx context.Context, username string) (*domain.User, error) {
// use repository here
}
func NewService(i *do.Injector) (domain.UserService, error) {
repo := do.MustInvoke[domain.UserRepository](i)
return &service{repo: repo}, nil
}
user/handler.go
package user
import (
"example/domain"
"net/http"
"github.com/samber/do"
)
type handler struct {
svc domain.UserService
}
func (h *handler) FetchByUsername() http.HandlerFunc {
// use service here
}
func NewHandler(i *do.Injector) (domain.UserHandler, error) {
svc := do.MustInvoke[domain.UserService](i)
return &handler{svc: svc}, nil
}
Conclusion
In this article, we have learned how to use samber/do to provide dependency injection in Go. We have seen how to create a DI container, add dependencies to the container, and retrieve dependencies from the container. We have also seen how to use the container to manage the dependencies of an application. By using samber/do, we can create more modular, testable, and maintainable code, and take full advantage of Go’s new generics feature.
If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading!