Dependancy injection is a pattern where we pass down dependancies to initializers. This pattern is used to avoid global variables and to make code more testable.

For example in go, you can create a service which depends on a repository and pass the repository to the service initializer.

Suppose you have a directory like this

server
 ├─entities
 │  ├─data.go
 │  └─server.go
 ├─handler
 │  └─handler.go
 ├─service
 │  └─service.go
 └─storage
    └─storage.go
main.go

suppose you have a User struct in data.go

package entities

type User struct {
    UID      string `json:"uid"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

in server.go you can have interfaces for storage, service and handler.

package entities

type IStorage interface {
    CreateUserStorage(user User) err
    DeleteUserStorage(uid string) err
}

type IService interface {
    CreateUserService(user User) err
    DeleteUserService(uid string) err
}

type IHandler interface {
    CreateUserHandler(c *fiber.Ctx) err
    DeleteUserHandler(c *fiber.Ctx) err
}

In storage.go you can have a struct which implements Storage interface.

package storage

import "app/server/entities"

type Storage struct {
    db *sql.DB
}

func NewStorage(db *sql.DB) entities.IStorage {
    return &Storage{
        db: db,
    }
}

// Implement Storage interface for example
func (s *Storage) CreateUserStorage(user entities.User) err {
    panic("Not implemented")
    // fmt.Println("Create user in db")
}

In service.go you can have a struct which implements Service interface.

package service

import "app/server/entities"

type Service struct {
    storage entities.IStorage
}

func NewService(storage entities.IStorage) entities.IService {
    return &Service{
        storage: storage,
    }
}

// Implement Service interface
func (s *Service) CreateUserService(user entities.User) err {
    password := hasher.Hash(user.Password)

    user.Password = password

    err := s.storage.CreateUserStorage(user)
    if err != nil {
        return err
    }
}

In handler.go you can have a struct which implements Handler interface.

package handler

import "app/server/entities"

type Handler struct {
    service entities.IService
}

func NewHandler(service entities.IService) entities.IHandler {
    return &Handler{
        service: service,
    }
}

// Implement Handler interface
func (h *Handler) CreateUserHandler(c *fiber.Ctx) err {
    userData := new(entities.User)

    // get data from request body ( json in this example )
    if err := c.BodyParser(userData); err != nil {
        return err
    }

    err := h.service.CreateUserService(userData)
    if err != nil {
        errMsg := handleError(err)
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": errMsg,
        })
    }

    return c.JSON(fiber.Map{
        "message": "User created successfully",
    })
}

In main.go you can initialize all the dependancies and pass them down to initializers.

package main

import (
    "app/server/entities"
    "app/server/handler"
    "app/server/service"
    "app/server/storage"
)

func main() {
    // Initialize db
    db, err := sql.Open("sqlite3", "./db.sqlite")
    if err != nil {
        log.Fatal(err)
    }

    // Initialize storage
    appStorage := storage.NewStorage(db)
    appService := service.NewService(appStorage)
    appHandler := handler.NewHandler(appService)

    // Initialize server
    app := fiber.New()

    // Register routes
    app.Get("/user", appHandler.CreateUserHandler)
    app.Delete("/user/:uid", appHandler.DeleteUserHandler)

    // Start server
    app.Listen(":3000")
}

Why all the trouble ? well, this pattern makes code more testable and maintainable. You can easily mock dependancies and test each module individually.

you can use mockery to generate mocks for interfaces.

mockery --all --output ./mocks

then use the mocks in your tests.

func TestCreateUserService(t *testing.T) {
    mockStorage := new(mocks.IStorage)
    // mock the return value of CreateUserStorage
    mockStorage.On("CreateUserStorage", mock.Anything).Return(nil)

    service := NewService(mockStorage)

    err := service.CreateUserService(user)
    assert.Nil(t, err)
}

There you go, a clean and testable codebase.