jeudi, mai 15, 2025

L'injection de dépendances avec Golang

Lorsqu'on développe un service Web et/ou REST API en Go on adopte souvent et à juste titre une architecture logicielle Modèle-Vue-Contrôleur. Le découplage voulu par cette approche pose un dilemme quant à la gestion des dépendances entre les modules.

Un exemple typique est le cas où il est nécessaire de créer qu'une seule instance du module qui gère la connexion et les interactions avec la base de données.  C'est alors que l'injection de cette dépendance peut être utile afin d'éviter d'avoir recours à une variable globale.

Illustration de packages pour une application Web.

 

Dans cet exemple d'architecture, le package main est le point d'entrée de l'application.  Ce package écoute sur un port des requêtes et dirige celles-ci vers leurs fonctions de traitement appropriées qui sont définies dans le package handlers.
 
Le package handlers gère la logique métier en ayant recours à son tour aux fonctions interagissants avec la base de données (package db) et en s'occupe du rendu de la réponse grâce aux fonctions définies dans le package components.

Le package middleware quant à lui permet de traiter par exemple l'authentification et les logs côté serveur.

Injection de dépendance.

package main

import (
"go-website/db"
"go-website/handlers"
"go-website/middleware"
"log"
"net/http"
"os"

"github.com/joho/godotenv"
)

func main() {
// reading environment variables
err := godotenv.Load()
if err != nil {
      log.Fatal("Error loading .env file")
}
port := os.Getenv("PORT")
dbFilePath := os.Getenv("DB_FILE_PATH")

appDb := db.AppDatabase{}
err = appDb.Open(dbFilePath)
if err != nil {
      log.Fatalf("Error opening database %s", err.Error())
      return
}

appHandlers := handlers.AppHandler{DB: &appDb}

router := http.NewServeMux()
// more routing code here…

middlewareChain := middleware.MiddlewareChain(middleware.AuthMiddleware, middleware.RequestLoggerMiddleWare)
server := http.Server{
    Addr: ":" + port,
    Handler: middlewareChain(router),
}
log.Printf("Listening on port %s", port)
server.ListenAndServe()
}

Dans l'extrait ci-dessus, la structure AppDatabase du package db qui encapsule les fonctions relatives aux opérations sur la base de données est instanciée une fois et elle est ensuite injectée dans la structure AppHandler du package handlers.
Le package handlers peut alors utiliser les fonctions de AppDatabase sans à avoir à gérer la connexion à la base comme le démontre le code suivant:

package handlers

import (
"go-website/components"
"go-website/db"
"go-website/session"
"log"
"net/http"
)

type AppHandler struct {
DB *db.AppDatabase
}

func (h *AppHandler) User(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")

user, err := h.DB.GetUserByUserName(username)
if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
}
name  := user.Name

// extra code to handle logic

components.UserTemplate().Render(r.Context(), w)
}

// more handler code.


Quant au package DB il pourrait être comme suit:
 
package db

import (
"database/sql"
"log"

_ "github.com/mattn/go-sqlite3"
)

type AppDatabase struct {
DB *sql.DB
}

type User struct {
Id     int        `json:"id"`
Username   string     `json:"username"`
Name  string     `json:"name"`
}

func (ab *AppDatabase) Open(dbFilePath string) (err error) {
ab.DB, err = sql.Open("sqlite3", dbFilePath)
if err != nil {
    log.Fatalf("Cannot open database. %s", err.Error())
    return err
}

return ab.DB.Ping()
}

func (ab *AppDatabase) Close() {
ab.DB.Close()
}

func (ab *AppDatabase) GetUserByUserName(username string) (user User, err error) {
u := User{}
err = ab.DB.QueryRow("select id, username, name from users where username = ?", username).Scan(&u.Id, &u.Username, &u.Name)
if err != nil {
    log.Fatalf("GetUserByUserName QueryRow failed: %s", err.Error())
    return
}
return u, nil
}

Libellés : , ,