引入
前面我们写了这么一个简单的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 struct | internal/model/ | 模特 (Model):定义数据的长相 |
var DB *gorm.DB | internal/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
}










