service层该放跨数据源、带事务、需复用或含业务规则的逻辑,如创建订单并扣库存发消息;不该放单表crud、http响应处理、复杂sql、缓存操作或请求上下文依赖。

Service层该放什么,不该放什么
Service层不是“把handler里代码剪下来贴过去”的中转站。它只该封装**跨数据源、带事务、需复用或含业务规则**的逻辑。比如用户注册要同时写users表、发激活邮件、记录操作日志——这三件事耦合强、不能拆到DAO或Handler里,就该进Service。而单纯的SELECT * FROM users WHERE id = ?这种单表查询,直接放在DAO里更清晰。
常见错误是把所有数据库操作都塞进Service,结果Service变胖、DAO变空,反而破坏了分层意图。记住:DAO负责“怎么查”,Service负责“为什么查、查完干什么”。
如何定义Service接口与实现分离
Iris本身不强制接口,但解耦必须靠接口抽象。先定义UserService接口,再写UserServiceImpl实现,Handler里只依赖接口:
type UserService interface {
CreateUser(ctx iris.Context, u User) error
GetUserByID(id int) (*User, error)
}
<p>type UserServiceImpl struct {
db *sql.DB // 或 repository 接口
}</p><p>func (s *UserServiceImpl) CreateUser(ctx iris.Context, u User) error {
tx, _ := s.db.Begin()
// 事务内操作
if err := s.insertUser(tx, u); err != nil {
tx.Rollback()
return err
}
if err := s.sendActivationEmail(u.Email); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}</p>
这样做的好处是:测试时可注入mock实现;切换数据库(如从MySQL换到PostgreSQL)只需换实现,不改Handler;未来加缓存层也只需在Service实现里包一层cache.UserService。
Service间依赖怎么处理才不循环
多个Service互相调用容易形成A→B→C→A循环依赖。避免方式有二:
- 用事件机制解耦:比如
UserService创建完用户后,发布UserCreatedEvent,由EmailService和LogService各自监听处理,而非直接调用 - 通过参数传递依赖:Handler协调调用顺序,把
EmailService作为参数传给UserService.CreateUser(),而不是让UserService自己去初始化它
后者更轻量,适合中小项目;前者更适合需要异步、解耦彻底的场景。别在Service构造函数里直接new EmailService()——那是硬编码,不是解耦。
配置驱动的Service切换要注意什么
开发/测试/生产环境可能用不同实现(比如测试用内存DB,生产用MySQL),靠配置切换时,关键点在初始化入口(通常是main.go或app/init.go):
不要写:if env == "prod" { service = NewMySQLUserService() }——判断逻辑散落各处,难维护。
应该统一用工厂函数+配置项:
func NewUserService(cfg config.ServiceConfig) UserService {
switch cfg.UserDBType {
case "mysql":
return &UserServiceImpl{db: mysqlConn()}
case "memory":
return &UserMemoryService{}
default:
panic("unknown user db type")
}
}
Service层解耦最易被忽略的点,是忘了把**上下文(iris.Context)剥离出Service方法签名**。Service不该知道HTTP请求生命周期,它只应接收原始参数(如id int、email string)并返回领域对象或错误。Handler才是负责从ctx里取参数、设Header、写响应的地方。











