Goweb项目的分层架构

引入

前面我们写了这么一个简单的RESTful API接口

 package main
 ​
 import (
  "net/http"
 ​
  "github.com/gin-gonic/gin"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
 )
 ​
 type User struct {
  gorm.Model
  Name string
  Age  int
 }
 ​
 var DB *gorm.DB
 ​
 func initDB() {
  dsn := "root :123456@tcp(127.0.0.1:3306)/gin_rank?charset=stf8mb4&parseTime=True&loc=Local"
  var err error
  DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
  panic("数据路连接失败" + err.Error())
  }
  DB.AutoMigrate(&User{})
 }
 ​
 func CreateUser(c *gin.Context) {
  var user User
  if err := c.ShouldBindJSON(&user); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"errpr": err.Error()})
  return
  }
  /*存入数据库*/
  if err := DB.Create(&user).Error; err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败"})
  return
  }
  c.JSON(http.StatusCreated, gin.H{
  "msg":  "create successfully",
  "data": user,
  })
 }
 ​
 func GetUserList(c *gin.Context) {
  var users []User
  DB.Find(&users)
  c.JSON(http.StatusOK, gin.H{
  "data": users,
  })
 }
 ​
 func UpdateUser(c *gin.Context) {
  id := c.Param("id")
  var user User
  if err := DB.First(&user, id).Error; err != nil {
  c.JSON(http.StatusNotFound, gin.H{"error": "用户存在"})
  return
  }
  if err := c.ShouldBindJSON(&user); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()})
  return
  }
  DB.Save(&user)
  c.JSON(http.StatusOK, gin.H{
  "msg":  "update successfully",
  "data": user,
  })
 }
 ​
 func DeleteUser(c *gin.Context) {
  id := c.Param("id")
  if err := DB.Delete(&User{}, id).Error; err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
  return
  }
  c.JSON(http.StatusOK, gin.H{
  "msg": "delete successfully",
  })
 }
 ​
 func main() {
  initDB()
  r := gin.Default()
  v1 := r.Group("/v1")
  {
  v1.POST("/users", CreateUser)
  v1.GET("/users", GetUserList)
  v1.PUT("/users/:id", UpdateUser)
  v1.DELETE("/users/:id", DeleteUser)
  }
  r.Run(":8080")
 }

在企业实际开发中,如果我们要更换数据库从MySQL换成PostgreSQL,我们就要在几百行代码里面搜索,万一漏了一个地方就不好了,这时候,我们需要对代码进行分层架构,下面是一个大型项目的文件目录

 my-gin-app/
 ├── cmd/
 │   └── server/          # 项目入口,存放 main.go
 ├── configs/             # 配置文件 (config.yaml)
 ├── internal/            # 业务核心代码 (外部不可引用)
 │   ├── initialize/      # 初始化逻辑 (DB, Redis, 日志)
 │   ├── global/          # 全局变量 (存放 DB, Config 等指针)
 │   ├── model/           # 结构体定义 (GORM 模型)
 │   ├── dao/             # 数据访问层 (直接操作数据库的函数)
 │   ├── service/         # 业务逻辑层 (组合 DAO 完成复杂业务)
 │   ├── controller/      # 入口控制层 (处理 Gin 请求参数/返回)
 │   └── router/          # 路由定义 (URL 与 Controller 的映射)
 ├── pkg/                 # 公共工具类 (加密、时间处理)
 └── go.mod               # 依赖管理

初步分类

我们来看看不同的代码要放到哪里

原始代码搬往何处 (目录)角色定位
type User structinternal/model/模特 (Model):定义数据的长相
var DB *gorm.DBinternal/global/仓库 (Global):存放大家都要用的公共对象
func initDB()internal/initialize/开关 (Init):项目启动时的准备工作
func CreateUser...internal/controller/前台 (Controller):接待请求,喊人办事
v1 := r.Group...internal/router/导视牌 (Router):指引请求去对应的入口

这里就不做演示了(不是ttdr懒

Viper

我们把这些功能代码,分类之后,我们回顾一个问题,我们如果把上面分类的好的代码传上去,那么emm全世界都知道你的账号密码了,而且在项目实际开发中,为了避免造成,莫名其妙的bug,运维在部署的时候,只想东你的yaml文件,这个时候configs/文件夹的作用体现了,我们需要一个配置的管家,来管理这些东西,那就是Viper

基本介绍

Viper 是 Go 语言中最强大的配置管理库。它的职责非常单纯:

  • 读取文件:支持 JSON, TOML, YAML, HCL 等格式。
  • 默认值:可以给配置项设置默认值。
  • 环境变量:可以自动读取环境变量(云原生部署必备)。
  • 实时监控:修改配置文件后,程序可以不重启就感知到变化。

安装

 go get github.com/spf13/viper

配置

 server:
   port: 8080
 ​
 mysql:
   path: "127.0.0.1:3306"
   config: "charset=utf8mb4&parseTime=True&loc=Local"
   db-name: "gin_rank"
   user: "root"
   password: "your_password"

然后我们要配置定义结构体,新建文件:internal/model/config.go

 package model
 ​
 type Config struct {
  Server Server `mapstructure:server"`
  Mysql  Mysql  `mapstructure:mysql`
 }
 type Server struct {
  Port int `mapstructure:"port"`
 }
 type Mysql struct {
  Path     string `mapstructure:"path"`
  Config   string `mapstructure:"config"`
  DbName   string `mapstructure:"db-name"`
  User     string `mapstructure:"user"`
  Password string `mapstructure:"password"`
 }

增加全局变量Config model.Config

增加初始化:internal/initialize/config.go

 package initialize
 ​
 import (
  "fmt"
  "my-gin-app/internal/global"
 ​
  "github.com/spf13/viper"
 )
 ​
 func InitConfig() {
  v := viper.New()
  v.SetConfigFile("configs/config.yaml")
  if err := v.ReadInConfig(); err != nil {
  panic(fmt.Errorf("读取配置失败:%s", err))
  }
  if err := v.Unmarshal(&global.Config); err != nil {
  panic(fmt.Errorf("解析配置失败:%s", err))
  }
 }

我们在货过去调数据库初始化代码

 package initialize
 ​
 /*数据库初始化*/
 ​
 import (
  "fmt"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "my-gin-app/internal/global"
  "my-gin-app/internal/model"
 )
 ​
 func InitDB() {
  m:= global.Config.Mysql
  dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", m.User,m.Password,m.Path,m.DbName,m.Config)
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
  panic("数据库连接失败: " + err.Error())
  }
  global.DB = db
  global.DB.AutoMigrate(&model.User{})
 }
 ​

DAO层分离

我们现在把功能分离到了Controller文件夹内了,里面的数据库操作代码,如果我们要对相关功能进行拓展,或者更改数据库,那么就要在代码里面寻找数据库操作,我们这里把SQL相关操作分离到DAO内。

基本介绍

DAO 层(也叫 Repository 层)是应用程序和数据源之间的中间人

  • 输入:业务数据(Model 结构体)。
  • 动作:执行具体的 SQL(Create, Find, Save, Delete)。
  • 输出:数据结果或错误信息。

分层后的调用链User (HTTP请求) -> Controller -> DAO -> DB

代码实现

 package dao
 ​
 import (
  "my-gin-app/internal/global"
  "my-gin-app/internal/model"
 )
 ​
 type UserDao struct{}
 ​
 /*方便后续拓展*/
 var User = new(UserDao)
 ​
 /*封装GORM操作*/
 func (d *UserDao) Create(user *model.User) error {
  return global.DB.Create(user).Error
 }
 func (d *UserDao) GetList() ([]model.User, error) {
  var users []model.User
  err := global.DB.Find(&users).Error
  return users, err
 }
 func (d *UserDao) Update(id int, user *model.User) error {
  user.ID = uint(id)
  return global.DB.Save(user).Error
 ​
 }
 func (d *UserDao) Delete(id int) error {
  return global.DB.Delete(&model.User{}, id).Error
 }
 ​

controller/user.go

 package controller
 ​
 /*入口控制层 (处理 Gin 请求参数/返回)*/
 import (
  "my-gin-app/internal/dao"
  "my-gin-app/internal/model"
  "net/http"
  "strconv"
 ​
  "github.com/gin-gonic/gin"
 )
 ​
 func CreateUser(c *gin.Context) {
  var user model.User
  if err := c.ShouldBindJSON(&user); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
  }
  /*存入数据库*/
  if err := dao.User.Create(&user); err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败"})
  return
  }
  c.JSON(http.StatusCreated, gin.H{
  "msg":  "create successfully",
  "data": user,
  })
 }
 func GetUserList(c *gin.Context) {
  users, err := dao.User.GetList()
  if err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "获取列表失败"})
  return
  }
  c.JSON(http.StatusOK, gin.H{"data": users})
 }
 func UpdateUser(c *gin.Context) {
  idStr := c.Param("id")
  id, _ := strconv.Atoi(idStr)
  var user model.User
  if err := c.ShouldBindJSON(&user); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()})
  return
  }
  if err := dao.User.Update(id, &user); err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
  return
  }
  c.JSON(http.StatusOK, gin.H{
  "msg":  "update successfully",
  "data": user,
  })
 }
 ​
 func DeleteUser(c *gin.Context) {
  idStr := c.Param("id")
  id, _ := strconv.Atoi(idStr)
  if err := dao.User.Delete(id); err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
  return
  }
  c.JSON(http.StatusOK, gin.H{
  "msg": "delete successfully",
  })
 }

现在我们实现了一个标准的MVC结构

但是我们的service还是空空如也,我们这里面要放什么呢

Service(核心业务逻辑层)

引入

前面处理完后 Controller 还是承担了一些它不该承担的责任(Controller 本职工作只是“解析参数”)? 比如:“id 字符串转 int”,“如果更新失败要不要重试”,用户注册时,如果用户名已经存在,要提示错误;且用户的年龄不能小于 0 岁。”?如果这些逻辑都写在 Controller 里,Controller 还是会爆炸。这就需要引入架构中最核心的一层:Service 层(业务逻辑层)。

基本介绍

现在的调用链将变成标准的 三层架构

Request -> [Controller] (参数解析) -> [Service] (业务逻辑) -> [DAO] (SQL操作) -> DB

  • Controller:处理 gin.Context,把 JSON 转成 Struct。
  • Service处理业务规则。比如“查重”、“加密密码”、“计算金额”。它不应该包含任何 gin 的代码(不依赖 HTTP)。
  • DAO:只做 CRUD。

代码实现

我们先增加一个根据名字查询的功能。

internal/dao/user.go

 func (d *UserDao)GetByName(name string)(model.User,error){
     var user model.User
     err := global.DB.Where("name = ?",name).First(&user).Error
     return user,err
 }

internal/dao/user.go

 package service
 ​
 import (
  "errors"
  "my-gin-app/internal/dao"
  "my-gin-app/internal/model"
 ​
  "gorm.io/gorm"
 )
 ​
 type UserService struct {}
 var User = new(UserService)
 /*创建新用户业务逻辑*/
 func (s *UserService)CreateUser(user *model.User)error {
  /*查一下是否存在*/
  existUser,err := dao.User.GetByName(user.Name)
  if err == nil && existUser.ID > 0{
  return errors.New("用户名已存在")
  }
  if err != nil && !errors.Is(err,gorm.ErrRecordNotFound){
  return err
  }
  if user.Age < 0{
  return errors.New("年龄不能为负数")
  }
  return dao.User.Create(user)
 }

internal/controller/user.go

 func CreateUser(c *gin.Context) {
  var user model.User
  if err := c.ShouldBindJSON(&user); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
  }
  if err := service.User.CreateUser(&user); err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  return
  }
  c.JSON(http.StatusCreated, gin.H{
  "msg":  "create successfully",
  "data": user,
  })
 }

底层哲学

胖 Service,瘦 Controller

这是企业级开发的一个黄金法则:Fat Service, Skinny Controller

  • Skinny Controller:Controller 的代码应该越少越好。如果你的 Controller 超过了 50 行,通常说明你把业务逻辑写错地方了。
  • Fat Service:所有的脏活累活(事务处理、第三方 API 调用、复杂计算)都应该封装在 Service 里。这样你的核心逻辑就和 HTTP 协议解耦了,即使以后你要换成 GRPC 协议,Service 层的代码也能直接复用

路由拆分和模块化

引入

项目过于复杂,我们要把路由都写在一个函数里,文件会变成几千行,会发生很多问题。我们需要把路由拆开,SetupRouter只负责去组装。

代码实现

internal/router/user.go

 package router
 ​
 import (
  "github.com/gin-gonic/gin"
  "my-gin-app/internal/controller"
 )
 ​
 func InitUserRoutes(r *gin.RouterGroup) {
  userGroup := r.Group("/users")
  {
  userGroup.POST("", controller.CreateUser)
  userGroup.GET("", controller.GetUserList)
  userGroup.PUT("/:id", controller.UpdateUser)
  userGroup.DELETE("/:id", controller.DeleteUser)
  }
 }

internal/router/route.go

 package router
 ​
 import (
  "github.com/gin-gonic/gin"
 )
 ​
 func SetupRouter() *gin.Engine {
  r := gin.Default()
 ​
  // 全局中间件(比如跨域、日志)可以在这里配置
 ​
  v1 := r.Group("/v1")
  {
  InitUserRoutes(v1)
  }
  return r
 }
暂无评论

发送评论 编辑评论


				
上一篇
下一篇