Service层应独立于Fiber框架,置于internal/service/目录下,使用纯Go结构体实现,方法签名只接收原始参数和依赖(如*sql.DB),不依赖fiber.Ctx;Handler层负责解析ctx、调用Service、处理响应。

Service 层该放在哪,和 Fiber 路由怎么解耦
Fiber 本身不规定分层结构,但硬把业务逻辑塞进 app.Get("/user", handler) 里,很快会失控。Service 层不是可选配件,而是隔离 HTTP 细节、复用核心逻辑的必经环节。它应该独立于 fiber.Ctx,不依赖任何框架类型。
推荐目录结构:internal/service/user_service.go(纯 Go 结构体+方法),internal/handler/user_handler.go(只做参数解析、ctx 状态控制、调用 service、返回响应)。
常见错误:在 service 方法签名里写 func (s *UserService) Get(ctx *fiber.Ctx) error —— 这会让 service 无法被 CLI 命令、定时任务或单元测试直接调用。
- service 方法只接收原始参数(如
id int64、req CreateUserReq)和依赖项(如*sql.DB或接口) - handler 负责从
ctx.Params("id")、ctx.Body()提取数据,校验后转成 service 所需类型 - service 返回领域模型或 error,不碰
ctx.Status()或ctx.JSON()
如何向 Service 注入依赖(DB、Cache、Logger)
别在 service 初始化时全局 new 一个 *sql.DB,更不要用单例包变量。Fiber 应用启动时应构造依赖树,再逐层注入——这是解耦和可测性的基础。
立即学习“go语言免费学习笔记(深入)”;
典型做法:定义依赖容器结构体,启动时初始化并传递给 service:
type AppDependencies struct {
DB *sql.DB
Cache *redis.Client
Logger *zerolog.Logger
}
func NewUserService(deps AppDependencies) *UserService {
return &UserService{
db: deps.DB,
cache: deps.Cache,
logger: deps.Logger.With().Str("component", "user_service").Logger(),
}
}
容易踩的坑:
- 用
var db *sql.DB包级变量 +init()初始化 —— 测试时无法替换 mock - service 内部调用
sql.Open()—— 连接池生命周期失控,panic 难追踪 - logger 直接用
log.Printf—— 丢失上下文字段,无法结构化采集
Service 方法要不要返回 error,还是自定义 Result 类型
Go 的惯用法是返回 (T, error),不是封装成 Result[T]。Fiber 场景下尤其如此:handler 需要根据 error 类型决定 HTTP 状态码(如 ErrUserNotFound → 404,ErrInvalidInput → 400),而统一 Result 会模糊错误语义。
建议做法:
- 定义清晰的错误变量,用
errors.Is()判断:var ErrUserNotFound = errors.New("user not found") - service 方法签名保持
func (s *UserService) GetUser(id int64) (*User, error) - handler 中用 switch 匹配错误:
if errors.Is(err, ErrUserNotFound) { ctx.Status(fiber.StatusNotFound).SendString("not found") } - 避免在 service 里调用
fmt.Errorf("failed to get user: %w", err)包裹原始错误 —— 会丢失底层错误类型,破坏errors.Is判断
事务怎么在 Service 和 Handler 之间传递
事务必须由 handler(或中间件)启动,再作为依赖传入 service 方法 —— 不是 service 自己开事务。否则无法跨多个 service 调用(如 “创建订单 + 扣减库存” 必须在同一个 tx)。
示例流程:
// handler
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
user, err := s.userService.Create(tx, req)
if err != nil {
return err
}
order, err := s.orderService.Create(tx, user.ID, req.Items)
if err != nil {
return err
}
return tx.Commit()
关键点:
- service 方法签名显式接收
tx *sql.Tx,而非*sql.DB - service 内部所有 DB 操作调用
tx.QueryRow()、tx.Exec(),不是s.db.QueryRow() - 不要在 service 里 commit/rollback —— 职责错位,且可能提前释放 tx
- 如果 service 需要同时支持 DB 和 tx 调用,可定义接口:
type Querier interface { QueryRow(...); Exec(...) },让*sql.DB和*sql.Tx都实现它
事务边界模糊是线上数据不一致最常见源头之一,这里多花两分钟设计参数,比事后查 binlog 强十倍。


















