应用分层
应用分层
go-web 的 app/ 目录采用经典四层架构 + DTO,目标是让每一层只做一件事。本章给出每一层的职责、典型代码风格,以及统一响应 / 错误码的使用方式。
HTTP 请求
│
▼
┌──────────────┐ 接受 RouterContextInterface
│ Controller │ 解析参数 / 调用 Service / 包装响应
└──────────────┘
│
▼
┌──────────────┐ 业务逻辑、事务编排
│ Service │ 不关心 HTTP / 不直接操作数据库
└──────────────┘
│
▼
┌──────────────┐ 封装 GORM 调用
│ DAO │ 返回 Model / 接收 Model
└──────────────┘
│
▼
┌──────────────┐ GORM 结构体
│ Model │ 数据库表的 Go 表示
└──────────────┘DTO(Data Transfer Object)横跨 Controller 与外部接口之间,负责定义 请求参数 与 响应结构。Model 则用于内部各层之间传递数据。
Controller —— app/controller/
- 接受
httpInterfaces.RouterContextInterface,不引用gin.Context - 嵌入
BaseResponse即可使用统一响应方法 - 不写业务逻辑,只做参数解析 + 响应包装
- 一个文件一个 Controller,例如
user_controller.go
package controller
import (
"cnb.cool/mliev/open/go-web/app/constants"
"cnb.cool/mliev/open/go-web/app/dto"
"cnb.cool/mliev/open/go-web/app/service"
httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
)
type UserController struct {
BaseResponse
}
func (receiver UserController) Create(c httpInterfaces.RouterContextInterface) {
var req dto.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
receiver.Error(c, constants.ErrCodeBadRequest, err.Error())
return
}
user, err := service.UserService{}.Create(req)
if err != nil {
receiver.Error(c, constants.ErrCodeInternal, err.Error())
return
}
receiver.Success(c, user)
}Service —— app/service/
- 业务逻辑、事务、领域规则
- 通过 DAO 操作数据库,通过 helper 访问 Logger / Cache / Redis
- 不依赖 HTTP 层接口
package service
import (
"cnb.cool/mliev/open/go-web/app/dao"
"cnb.cool/mliev/open/go-web/app/dto"
"cnb.cool/mliev/open/go-web/app/model"
)
type UserService struct{}
func (UserService) Create(req dto.CreateUserRequest) (*model.User, error) {
user := &model.User{
Username: req.Username,
Email: req.Email,
}
if err := (dao.UserDao{}).Create(user); err != nil {
return nil, err
}
return user, nil
}DAO —— app/dao/
- 封装 GORM 调用
- 一个 DAO 对应一个 Model
- 不写业务规则
package dao
import (
"cnb.cool/mliev/open/go-web/app/model"
"cnb.cool/mliev/open/go-web/pkg/helper"
)
type UserDao struct{}
func (UserDao) Create(u *model.User) error {
return helper.GetDatabase().Create(u).Error
}
func (UserDao) GetByID(id uint) (*model.User, error) {
var u model.User
if err := helper.GetDatabase().First(&u, id).Error; err != nil {
return nil, err
}
return &u, nil
}Model —— app/model/
GORM 结构体。建议每个表对应一个文件:
package model
import "time"
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"size:50;uniqueIndex"`
Email string `gorm:"size:120"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (User) TableName() string { return "users" }DTO —— app/dto/
请求体与响应体定义,使用 binding tag 做参数校验:
package dto
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
}
// 通用响应包装,由 BaseResponse 使用
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}BaseResponse 统一响应
app/controller/base_response.go 提供四个方法:
// 成功 → {"code":0,"message":"操作成功","data":...}
receiver.Success(c, data)
// 自定义成功消息
receiver.SuccessWithMessage(c, "已创建", data)
// 错误 → {"code":<code>,"message":<msg>}
receiver.Error(c, constants.ErrCodeBadRequest, "参数错误")
// 带数据的错误响应
receiver.ErrorWithData(c, constants.ErrCodeBadRequest, "参数校验失败", validationErrors)HTTP 状态码规则:当 code ∈ [400, 600) 时,HTTP 状态码与业务码一致;否则 HTTP 状态码为 200。这意味着 401 / 404 / 500 等业务码会触发对应的 HTTP 状态,而自定义业务码(例如 1001)默认 200,由前端根据 code 判断。
错误码 —— app/constants/errors.go
内置一组与 HTTP 状态码对齐的错误码:
const (
ErrCodeSuccess = 0
ErrCodeBadRequest = 400
ErrCodeUnauthorized = 401
ErrCodeForbidden = 403
ErrCodeNotFound = 404
ErrCodeConflict = 409
ErrCodeInternal = 500
ErrCodeBadGateway = 502
ErrCodeUnavailable = 503
)constants.GetErrMessage(code) 返回对应的中文描述。需要扩展时,直接在 errors.go 中追加新常量与 ErrMessages 项即可。
中间件 —— app/middleware/
业务相关的中间件放在这里(认证、限流、CORS……),框架级中间件由 HTTP server 自动挂载。
package middleware
import (
"net/http"
httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
)
func AuthMiddleware() httpInterfaces.MiddlewareFunc {
return func(c httpInterfaces.RouterContextInterface) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set("userId", "12345")
c.Next()
}
}详细的注册方式见「路由」章节。