Structure d'un projet Go Web avec Exemples de Code

Ci-dessous, une architecture de projet Go Web est présentée via un diagramme de paquets pour clarifier l'organisation.

Étant passé de Java à Go, on retrouve une structure similaire. Java utilise Controller, Service, Repository ; Go utilise api, service, dao. Bien que les philosophies de conception diffèrent, l'objectif reste le découplage via l'architecture.

Configuration des Routes

Créez le fichier routes/router.go. Il définit les URI des API, les routes, les groupes de routes et l'ajout de middlewares.

func InitRouter() http.Handler {
    r := gin.New()
    r.SetTrustedProxies([]string{"*"})
    r.Use(middleware.Logger())
    r.Use(middleware.ErrorRecovery(false))
    r.Use(middleware.Cors())

    // Routes nécessitant une authentification
    auth := base.Group("/admin")
    auth.Use(middleware.JWTAuth())
    auth.Use(middleware.RBAC())
    auth.Use(middleware.ListenOnline())
    auth.Use(middleware.OperationLog())

    page := auth.Group("/page")
    {
        page.GET("/list", pageAPI.GetList)
        page.POST("", pageAPI.SaveOrUpdate)
        page.DELETE("", pageAPI.Delete)
    }
    return r
}

Configuration de la Base de Données

func InitMySQLDB() *gorm.DB {
    mysqlCfg := config.Cfg.Mysql
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        mysqlCfg.Username,
        mysqlCfg.Password,
        mysqlCfg.Host,
        mysqlCfg.Port,
        mysqlCfg.Dbname,
    )
    db, err := gorm.Open(mysql.Open(dsn), gormConfig())
    if err != nil {
        log.Fatal("Connexion MySQL échouée, vérifiez les paramètres")
    }
    log.Println("Connexion MySQL réussie")
    sqlDB, _ := db.DB()
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(10 * time.Second)
    return db
}

func gormConfig() *gorm.Config {
    return &gorm.Config{
        Logger: logger.Default.LogMode(getLogMode(config.Cfg.Mysql.LogMode)),
        DisableForeignKeyConstraintWhenMigrating: true,
        SkipDefaultTransaction: true,
        NamingStrategy: schema.NamingStrategy{
            SingularTable: true,
        },
    }
}

Normes de Codage

Couche API

Dans routes/router.go, un exemple de route :

auth.GET("/home", blogInfoAPI.GetHomeInfo)

La couche API est simple, elle appelle simplement le service :

api/v1/home.go

func (*BlogInfo) GetHomeInfo(c *gin.Context) {
    r.SuccessData(c, blogInfoService.GetHomeInfo())
}

Réponse RESTful

Dans utils/r/result.go, des méthodes sont implémentées sur *gin.Context via un pointeur, permettant un enchaînement sans retour :

func ReturnJson(c *gin.Context, httpCode, code int, msg string, data any) {
    c.JSON(httpCode, Response{
        Code:    code,
        Message: msg,
        Data:    data,
    })
}

func Send(c *gin.Context, httpCode, code int, data any) {
    ReturnJson(c, httpCode, code, GetMsg(code), data)
}

func SendCode(c *gin.Context, code int) {
    Send(c, http.StatusOK, code, nil)
}

func SendData(c *gin.Context, code int, data any) {
    Send(c, http.StatusOK, code, data)
}

func SuccessData(c *gin.Context, data any) {
    Send(c, http.StatusOK, OK, data)
}

func Success(c *gin.Context) {
    Send(c, http.StatusOK, OK, nil)
}

Couche Service

Les services retournent des VO (View Objects) pour la sérialisation JSON. Ces VO sont généralement placés dans le modèle, mais on pourrait les déplacer dans le paquet service pour plus de clarté.

func (*Article) GetInfo(id int) resp.ArticleDetailVO {
    article := dao.GetOne(model.Article{}, "id", id)
    category := dao.GetOne(model.Category{}, "id", article.CategoryId)
    tagNames := tagDao.GetTagNamesByArtId(id)

    articleVo := utils.CopyProperties[resp.ArticleDetailVO](article)
    if category.ID != 0 {
        articleVo.CategoryName = &category.Name
    }
    articleVo.TagNames = tagNames
    return articleVo
}

Couche DAO

Remarques :

  • Les fonctions non génériques doivent être des méthodes d'une structure spécifique.
  • Les fonctions génériques peuvent utiliser des types paramétrés.

Attribution de dao.DB

Déclarez var DB *gorm.DB dans les fichiers du paquet dao. Dans routes/router.go, initialisez-la :

func InitGlobalVariable() {
    utils.InitViper()
    utils.InitLogger()
    dao.DB = utils.InitMySQLDB()
    utils.InitRedis()
    utils.InitCasbin(dao.DB)
}

Fonction Générique

func Create[T any](data *T) {
    err := DB.Create(&data).Error
    if err != nil {
        panic(err)
    }
}

Méthode Spécifique

func (*Article) GetInfoById(id int) (res resp.FrontArticleDetailVO) {
    DB.Table("article").
        Preload("Category").
        Preload("Tags").
        Where("id = ? AND is_delete = 0 AND status = 1", id).
        First(&res)
    return
}

Middlewares

Ils interceptent les requêtes pour vérifier les autorisations. Un groupe de routes basé sur un URI racine sert de point de vérification.

auth := base.Group("/admin")
auth.Use(middleware.JWTAuth())
auth.Use(middleware.RBAC())
auth.Use(middleware.ListenOnline())
auth.Use(middleware.OperationLog())

Gestion CORS

func Cors() gin.HandlerFunc {
    return cors.New(cors.Config{
        AllowOrigins:     []string{"*"},
        AllowMethods:     []string{"PUT", "POST", "GET", "DELETE", "OPTIONS", "PATCH"},
        AllowHeaders:     []string{"Origin", "Authorization", "Content-Type", "X-Requested-With"},
        ExposeHeaders:    []string{"Content-Type"},
        AllowCredentials: true,
        AllowOriginFunc:  func(origin string) bool { return true },
        MaxAge:           24 * time.Hour,
    })
}

Résumé sur les Middlewares

En Go, une fonction peut être une valeur. La structure d'un middleware est :

func XXX() gin.HandlerFunc {
    return func(c *gin.Context) {
        // logique du middleware
    }
}

On l'attache à un groupe :

sys := router.Group("/sys")
sys.Use(middleware.XXX())

Technologie de Template

router.LoadHTMLGlob("template/*")

router.GET("/index", func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.html", gin.H{"title": "Je suis un test", "ce": "123456"})
})

Template :

<html>
<head>
    <title>{{.title}}</title>
</head>
<body>
    Numéro : <h1>{{.ce}}</h1>
</body>
</html>

Modularisation avec en-tête et pied de page :

{{ define "user/index.html" }}
{{template "public/header" .}}
    Adresse : {{.address}}
{{template "public/footer" .}}
{{ end }}

Étiquettes: Go gin gorm middleware CORS

Publié le 6 juillet à 02h41