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 }}